Prerequisites
- Strong understanding of REST APIs and HTTP methods 📝
- Experience with TypeScript interfaces and generics ⚡
- Basic knowledge of OpenAPI/Swagger specifications 💻
What you'll learn
- Master OpenAPI specification for comprehensive API documentation 🎯
- Generate type-safe TypeScript clients from OpenAPI schemas 🏗️
- Build automated workflows for API type synchronization 🐛
- Create enterprise-ready API development patterns with tooling ✨
🎯 Introduction
Welcome to the future of API development! 🔧 If manual type definition is like hand-crafting each screw for a machine, then OpenAPI type generation is like having an automated factory that produces perfectly fitting TypeScript types from your API specifications!
OpenAPI (formerly Swagger) specifications serve as the single source of truth for your REST APIs, and with modern tooling, you can automatically generate TypeScript types, client libraries, and even complete SDK packages. This approach eliminates type mismatches between frontend and backend, reduces maintenance overhead, and ensures your API contracts are always up-to-date.
By the end of this tutorial, you’ll be a master of API type generation, capable of building completely automated workflows that keep your TypeScript applications perfectly synchronized with your REST API specifications. Let’s generate some type safety! ⚡
📚 Understanding OpenAPI and Type Generation
🤔 What Is OpenAPI?
OpenAPI Specification (OAS) is a standard for describing REST APIs. It defines endpoints, request/response schemas, authentication methods, and more in a machine-readable format that can be used to generate documentation, client SDKs, and TypeScript types.
# 🌟 Basic OpenAPI specification
openapi: 3.0.3
info:
title: User Management API
description: A comprehensive API for managing users and their data
version: 1.0.0
contact:
name: API Support
email: [email protected]
servers:
- url: https://api.example.com/v1
description: Production server
- url: https://staging-api.example.com/v1
description: Staging server
# 📦 Reusable components
components:
schemas:
User:
type: object
required:
- id
- email
- name
properties:
id:
type: integer
format: int64
description: Unique identifier for the user
example: 12345
email:
type: string
format: email
description: User's email address
example: [email protected]
name:
type: string
minLength: 1
maxLength: 100
description: User's full name
example: John Doe
age:
type: integer
minimum: 0
maximum: 150
description: User's age in years
example: 30
role:
type: string
enum: [admin, user, moderator]
description: User's role in the system
example: user
isActive:
type: boolean
description: Whether the user account is active
example: true
metadata:
type: object
additionalProperties: true
description: Additional user metadata
createdAt:
type: string
format: date-time
description: When the user was created
example: "2023-01-01T12:00:00Z"
updatedAt:
type: string
format: date-time
description: When the user was last updated
example: "2023-01-02T12:00:00Z"
CreateUserRequest:
type: object
required:
- email
- name
properties:
email:
type: string
format: email
description: User's email address
name:
type: string
minLength: 1
maxLength: 100
description: User's full name
age:
type: integer
minimum: 0
maximum: 150
description: User's age in years
role:
type: string
enum: [admin, user, moderator]
default: user
description: User's role in the system
metadata:
type: object
additionalProperties: true
description: Additional user metadata
UpdateUserRequest:
type: object
properties:
name:
type: string
minLength: 1
maxLength: 100
description: User's full name
age:
type: integer
minimum: 0
maximum: 150
description: User's age in years
role:
type: string
enum: [admin, user, moderator]
description: User's role in the system
isActive:
type: boolean
description: Whether the user account is active
metadata:
type: object
additionalProperties: true
description: Additional user metadata
ApiResponse:
type: object
required:
- success
- message
properties:
success:
type: boolean
description: Whether the operation was successful
message:
type: string
description: Human-readable message about the operation
data:
description: The response data (varies by endpoint)
errors:
type: array
items:
type: string
description: List of error messages if operation failed
PaginatedResponse:
type: object
required:
- data
- pagination
properties:
data:
type: array
description: Array of items for the current page
pagination:
type: object
required:
- page
- limit
- total
- pages
properties:
page:
type: integer
minimum: 1
description: Current page number
limit:
type: integer
minimum: 1
maximum: 100
description: Number of items per page
total:
type: integer
minimum: 0
description: Total number of items
pages:
type: integer
minimum: 0
description: Total number of pages
parameters:
PageParam:
name: page
in: query
description: Page number for pagination
required: false
schema:
type: integer
minimum: 1
default: 1
LimitParam:
name: limit
in: query
description: Number of items per page
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 20
UserIdParam:
name: userId
in: path
description: User ID
required: true
schema:
type: integer
format: int64
# 🎯 API endpoints
paths:
/users:
get:
summary: List users
description: Retrieve a paginated list of users
operationId: listUsers
tags:
- Users
parameters:
- $ref: '#/components/parameters/PageParam'
- $ref: '#/components/parameters/LimitParam'
- name: role
in: query
description: Filter users by role
required: false
schema:
type: string
enum: [admin, user, moderator]
- name: isActive
in: query
description: Filter users by active status
required: false
schema:
type: boolean
- name: search
in: query
description: Search users by name or email
required: false
schema:
type: string
minLength: 1
responses:
'200':
description: List of users retrieved successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
data:
allOf:
- $ref: '#/components/schemas/PaginatedResponse'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
'400':
description: Bad request - invalid parameters
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
post:
summary: Create user
description: Create a new user account
operationId: createUser
tags:
- Users
requestBody:
description: User data for creation
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
data:
$ref: '#/components/schemas/User'
'400':
description: Bad request - validation errors
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'409':
description: Conflict - user already exists
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
/users/{userId}:
get:
summary: Get user by ID
description: Retrieve a specific user by their ID
operationId: getUserById
tags:
- Users
parameters:
- $ref: '#/components/parameters/UserIdParam'
responses:
'200':
description: User retrieved successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
data:
$ref: '#/components/schemas/User'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
put:
summary: Update user
description: Update an existing user's information
operationId: updateUser
tags:
- Users
parameters:
- $ref: '#/components/parameters/UserIdParam'
requestBody:
description: Updated user data
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateUserRequest'
responses:
'200':
description: User updated successfully
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/ApiResponse'
- type: object
properties:
data:
$ref: '#/components/schemas/User'
'400':
description: Bad request - validation errors
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
delete:
summary: Delete user
description: Delete a user account
operationId: deleteUser
tags:
- Users
parameters:
- $ref: '#/components/parameters/UserIdParam'
responses:
'200':
description: User deleted successfully
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'404':
description: User not found
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
🛠️ Setting Up Type Generation Tools
📦 Installing OpenAPI Generators
There are several excellent tools for generating TypeScript types from OpenAPI specifications. Let’s explore the most powerful ones:
// 🌟 Package.json setup for type generation
{
"name": "api-client-example",
"version": "1.0.0",
"scripts": {
"generate:types": "openapi-typescript openapi.yaml --output ./src/types/api.ts",
"generate:client": "openapi-codegen",
"generate:all": "npm run generate:types && npm run generate:client",
"build": "npm run generate:all && tsc",
"dev": "npm run generate:all && tsc --watch"
},
"devDependencies": {
"@openapitools/openapi-generator-cli": "^2.7.0",
"openapi-typescript": "^6.7.1",
"openapi-typescript-codegen": "^0.25.0",
"swagger-typescript-api": "^13.0.3",
"orval": "^6.20.0",
"typescript": "^5.3.0",
"axios": "^1.6.0"
},
"dependencies": {
"ky": "^1.0.0"
}
}
🎯 OpenAPI TypeScript Generator
The most straightforward approach uses openapi-typescript
to generate type definitions:
// 🚀 Generated types from OpenAPI spec
// src/types/api.ts (generated automatically)
export interface paths {
"/users": {
get: operations["listUsers"];
post: operations["createUser"];
};
"/users/{userId}": {
get: operations["getUserById"];
put: operations["updateUser"];
delete: operations["deleteUser"];
};
}
export interface components {
schemas: {
User: {
id: number;
email: string;
name: string;
age?: number;
role?: "admin" | "user" | "moderator";
isActive?: boolean;
metadata?: Record<string, any>;
createdAt?: string;
updatedAt?: string;
};
CreateUserRequest: {
email: string;
name: string;
age?: number;
role?: "admin" | "user" | "moderator";
metadata?: Record<string, any>;
};
UpdateUserRequest: {
name?: string;
age?: number;
role?: "admin" | "user" | "moderator";
isActive?: boolean;
metadata?: Record<string, any>;
};
ApiResponse: {
success: boolean;
message: string;
data?: any;
errors?: string[];
};
PaginatedResponse: {
data: any[];
pagination: {
page: number;
limit: number;
total: number;
pages: number;
};
};
};
parameters: {
PageParam: number;
LimitParam: number;
UserIdParam: number;
};
}
export interface operations {
listUsers: {
parameters: {
query?: {
page?: number;
limit?: number;
role?: "admin" | "user" | "moderator";
isActive?: boolean;
search?: string;
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["ApiResponse"] & {
data: components["schemas"]["PaginatedResponse"] & {
data: components["schemas"]["User"][];
};
};
};
};
400: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
500: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
};
};
createUser: {
requestBody: {
content: {
"application/json": components["schemas"]["CreateUserRequest"];
};
};
responses: {
201: {
content: {
"application/json": components["schemas"]["ApiResponse"] & {
data: components["schemas"]["User"];
};
};
};
400: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
409: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
500: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
};
};
getUserById: {
parameters: {
path: {
userId: number;
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["ApiResponse"] & {
data: components["schemas"]["User"];
};
};
};
404: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
500: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
};
};
updateUser: {
parameters: {
path: {
userId: number;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["UpdateUserRequest"];
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["ApiResponse"] & {
data: components["schemas"]["User"];
};
};
};
400: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
404: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
500: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
};
};
deleteUser: {
parameters: {
path: {
userId: number;
};
};
responses: {
200: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
404: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
500: {
content: {
"application/json": components["schemas"]["ApiResponse"];
};
};
};
};
}
// 🎯 Utility types for easier usage
export type User = components["schemas"]["User"];
export type CreateUserRequest = components["schemas"]["CreateUserRequest"];
export type UpdateUserRequest = components["schemas"]["UpdateUserRequest"];
export type ApiResponse<T = any> = components["schemas"]["ApiResponse"] & { data?: T };
export type PaginatedResponse<T = any> = components["schemas"]["PaginatedResponse"] & { data: T[] };
// 📤 Request/Response types for each operation
export type ListUsersParams = operations["listUsers"]["parameters"]["query"];
export type ListUsersResponse = operations["listUsers"]["responses"]["200"]["content"]["application/json"];
export type CreateUserParams = operations["createUser"]["requestBody"]["content"]["application/json"];
export type CreateUserResponse = operations["createUser"]["responses"]["201"]["content"]["application/json"];
export type GetUserParams = operations["getUserById"]["parameters"]["path"];
export type GetUserResponse = operations["getUserById"]["responses"]["200"]["content"]["application/json"];
export type UpdateUserParams = operations["updateUser"]["parameters"]["path"] & {
body: operations["updateUser"]["requestBody"]["content"]["application/json"];
};
export type UpdateUserResponse = operations["updateUser"]["responses"]["200"]["content"]["application/json"];
export type DeleteUserParams = operations["deleteUser"]["parameters"]["path"];
export type DeleteUserResponse = operations["deleteUser"]["responses"]["200"]["content"]["application/json"];
🏗️ Building Type-Safe API Clients
🎯 Basic API Client Implementation
Now let’s create a type-safe API client using our generated types:
// 🌟 Type-safe API client implementation
// src/api/client.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import type {
User,
CreateUserRequest,
UpdateUserRequest,
ListUsersParams,
ListUsersResponse,
CreateUserResponse,
GetUserResponse,
UpdateUserResponse,
DeleteUserResponse,
ApiResponse,
} from '../types/api';
// 📦 Configuration interface
interface ApiClientConfig {
baseURL: string;
timeout?: number;
apiKey?: string;
retries?: number;
}
// 🚀 Type-safe API client class
export class UserApiClient {
private client: AxiosInstance;
private retries: number;
constructor(config: ApiClientConfig) {
this.retries = config.retries ?? 3;
this.client = axios.create({
baseURL: config.baseURL,
timeout: config.timeout ?? 10000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...(config.apiKey && { 'Authorization': `Bearer ${config.apiKey}` }),
},
});
this.setupInterceptors();
}
// 🔧 Setup request/response interceptors
private setupInterceptors(): void {
// Request interceptor
this.client.interceptors.request.use(
(config) => {
console.log(`📡 Making ${config.method?.toUpperCase()} request to ${config.url}`);
return config;
},
(error) => {
console.error('❌ Request error:', error);
return Promise.reject(error);
}
);
// Response interceptor
this.client.interceptors.response.use(
(response) => {
console.log(`✅ Response received: ${response.status} ${response.statusText}`);
return response;
},
async (error) => {
const originalRequest = error.config;
// Auto-retry logic
if (originalRequest && !originalRequest._retry && originalRequest._retryCount < this.retries) {
originalRequest._retry = true;
originalRequest._retryCount = (originalRequest._retryCount || 0) + 1;
console.log(`🔄 Retrying request (attempt ${originalRequest._retryCount}/${this.retries})`);
// Exponential backoff
const delay = Math.pow(2, originalRequest._retryCount) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return this.client(originalRequest);
}
console.error('❌ Response error:', error);
return Promise.reject(error);
}
);
}
// 🎯 Generic request method with full type safety
private async request<T>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
options: AxiosRequestConfig = {}
): Promise<T> {
try {
const response: AxiosResponse<T> = await this.client.request({
method,
url: endpoint,
...options,
});
return response.data;
} catch (error) {
if (axios.isAxiosError(error)) {
const errorMessage = error.response?.data?.message || error.message;
throw new Error(`API Error: ${errorMessage}`);
}
throw error;
}
}
// 📋 List users with type-safe parameters
async listUsers(params?: ListUsersParams): Promise<ListUsersResponse> {
console.log('👥 Fetching user list...');
const response = await this.request<ListUsersResponse>('GET', '/users', {
params,
});
console.log(`✅ Retrieved ${response.data.data.length} users`);
return response;
}
// 👤 Get user by ID
async getUserById(userId: number): Promise<GetUserResponse> {
console.log(`🔍 Fetching user ${userId}...`);
const response = await this.request<GetUserResponse>('GET', `/users/${userId}`);
console.log(`✅ Retrieved user: ${response.data.email}`);
return response;
}
// ➕ Create new user
async createUser(userData: CreateUserRequest): Promise<CreateUserResponse> {
console.log(`👤 Creating user: ${userData.email}...`);
const response = await this.request<CreateUserResponse>('POST', '/users', {
data: userData,
});
console.log(`✅ Created user with ID: ${response.data.id}`);
return response;
}
// ✏️ Update existing user
async updateUser(userId: number, userData: UpdateUserRequest): Promise<UpdateUserResponse> {
console.log(`📝 Updating user ${userId}...`);
const response = await this.request<UpdateUserResponse>('PUT', `/users/${userId}`, {
data: userData,
});
console.log(`✅ Updated user: ${response.data.email}`);
return response;
}
// 🗑️ Delete user
async deleteUser(userId: number): Promise<DeleteUserResponse> {
console.log(`🗑️ Deleting user ${userId}...`);
const response = await this.request<DeleteUserResponse>('DELETE', `/users/${userId}`);
console.log('✅ User deleted successfully');
return response;
}
// 🔍 Search users with advanced filtering
async searchUsers(searchTerm: string, filters?: Partial<ListUsersParams>): Promise<User[]> {
console.log(`🔍 Searching users with term: "${searchTerm}"...`);
const response = await this.listUsers({
search: searchTerm,
...filters,
});
console.log(`✅ Found ${response.data.data.length} matching users`);
return response.data.data;
}
// 📊 Get users with pagination support
async getUsersPaginated(page: number = 1, limit: number = 20): Promise<{
users: User[];
pagination: ListUsersResponse['data']['pagination'];
}> {
console.log(`📄 Fetching page ${page} with ${limit} users...`);
const response = await this.listUsers({ page, limit });
return {
users: response.data.data,
pagination: response.data.pagination,
};
}
}
// 🎮 Usage examples
const exampleUsage = async (): Promise<void> => {
// 🏗️ Initialize the API client
const apiClient = new UserApiClient({
baseURL: 'https://api.example.com/v1',
apiKey: 'your-api-key-here',
timeout: 15000,
retries: 3,
});
try {
// 📋 List all users
const userList = await apiClient.listUsers();
console.log('👥 All users:', userList.data.data);
// 🔍 Get specific user
const user = await apiClient.getUserById(123);
console.log('👤 User details:', user.data);
// ➕ Create new user
const newUser = await apiClient.createUser({
email: '[email protected]',
name: 'Jane Doe',
age: 28,
role: 'user',
metadata: {
department: 'Engineering',
location: 'San Francisco',
},
});
console.log('✅ Created user:', newUser.data);
// ✏️ Update user
const updatedUser = await apiClient.updateUser(newUser.data.id, {
name: 'Jane Smith',
role: 'moderator',
isActive: true,
});
console.log('📝 Updated user:', updatedUser.data);
// 🔍 Search users
const searchResults = await apiClient.searchUsers('Jane', {
role: 'moderator',
isActive: true,
});
console.log('🔍 Search results:', searchResults);
// 📄 Paginated results
const { users, pagination } = await apiClient.getUsersPaginated(1, 10);
console.log('📄 Page 1 users:', users);
console.log('📊 Pagination info:', pagination);
// 🗑️ Delete user
await apiClient.deleteUser(newUser.data.id);
console.log('🗑️ User deleted successfully');
} catch (error) {
console.error('💥 API Error:', error);
}
};
🎨 Advanced Client Features
Let’s enhance our API client with advanced features like caching, request deduplication, and real-time updates:
// 🚀 Advanced API client with caching and real-time features
// src/api/advanced-client.ts
import { EventEmitter } from 'events';
import type { User, CreateUserRequest, UpdateUserRequest, ListUsersParams } from '../types/api';
// 📦 Cache interface
interface CacheEntry<T> {
data: T;
timestamp: number;
ttl: number;
}
// 🎯 Advanced API client with caching
export class AdvancedUserApiClient extends EventEmitter {
private cache = new Map<string, CacheEntry<any>>();
private pendingRequests = new Map<string, Promise<any>>();
private defaultTTL = 5 * 60 * 1000; // 5 minutes
constructor(private baseClient: UserApiClient) {
super();
this.setupCacheCleanup();
}
// 🧹 Cache cleanup
private setupCacheCleanup(): void {
setInterval(() => {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
console.log(`🗑️ Cache entry expired: ${key}`);
}
}
}, 60000); // Clean up every minute
}
// 🔑 Generate cache key
private getCacheKey(method: string, endpoint: string, params?: any): string {
const paramString = params ? JSON.stringify(params) : '';
return `${method}:${endpoint}:${paramString}`;
}
// 💾 Get from cache
private getFromCache<T>(key: string): T | null {
const entry = this.cache.get(key);
if (!entry) return null;
const now = Date.now();
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return null;
}
console.log(`📋 Cache hit: ${key}`);
return entry.data;
}
// 💾 Set cache
private setCache<T>(key: string, data: T, ttl: number = this.defaultTTL): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl,
});
console.log(`💾 Cached: ${key}`);
}
// 🔄 Request deduplication
private async dedupedRequest<T>(
key: string,
requestFn: () => Promise<T>,
ttl?: number
): Promise<T> {
// Check cache first
const cached = this.getFromCache<T>(key);
if (cached) return cached;
// Check if request is already pending
const pending = this.pendingRequests.get(key);
if (pending) {
console.log(`⏳ Deduplicating request: ${key}`);
return pending;
}
// Make new request
const request = requestFn();
this.pendingRequests.set(key, request);
try {
const result = await request;
this.setCache(key, result, ttl);
return result;
} finally {
this.pendingRequests.delete(key);
}
}
// 👥 Cached user list
async listUsers(params?: ListUsersParams, options?: { ttl?: number; forceRefresh?: boolean }) {
const key = this.getCacheKey('GET', '/users', params);
if (options?.forceRefresh) {
this.cache.delete(key);
console.log(`🔄 Force refreshing: ${key}`);
}
return this.dedupedRequest(
key,
() => this.baseClient.listUsers(params),
options?.ttl
);
}
// 👤 Cached user by ID
async getUserById(userId: number, options?: { ttl?: number; forceRefresh?: boolean }) {
const key = this.getCacheKey('GET', `/users/${userId}`);
if (options?.forceRefresh) {
this.cache.delete(key);
console.log(`🔄 Force refreshing user: ${userId}`);
}
return this.dedupedRequest(
key,
() => this.baseClient.getUserById(userId),
options?.ttl
);
}
// ➕ Create user with cache invalidation
async createUser(userData: CreateUserRequest) {
console.log('👤 Creating user and invalidating cache...');
const result = await this.baseClient.createUser(userData);
// Invalidate relevant cache entries
this.invalidateUserListCache();
// Emit event for real-time updates
this.emit('userCreated', result.data);
return result;
}
// ✏️ Update user with cache invalidation
async updateUser(userId: number, userData: UpdateUserRequest) {
console.log(`📝 Updating user ${userId} and invalidating cache...`);
const result = await this.baseClient.updateUser(userId, userData);
// Invalidate specific user cache
const userKey = this.getCacheKey('GET', `/users/${userId}`);
this.cache.delete(userKey);
// Invalidate user list cache
this.invalidateUserListCache();
// Emit event for real-time updates
this.emit('userUpdated', result.data);
return result;
}
// 🗑️ Delete user with cache invalidation
async deleteUser(userId: number) {
console.log(`🗑️ Deleting user ${userId} and invalidating cache...`);
const result = await this.baseClient.deleteUser(userId);
// Invalidate specific user cache
const userKey = this.getCacheKey('GET', `/users/${userId}`);
this.cache.delete(userKey);
// Invalidate user list cache
this.invalidateUserListCache();
// Emit event for real-time updates
this.emit('userDeleted', userId);
return result;
}
// 🧹 Cache invalidation helpers
private invalidateUserListCache(): void {
console.log('🧹 Invalidating user list cache...');
for (const key of this.cache.keys()) {
if (key.startsWith('GET:/users:')) {
this.cache.delete(key);
}
}
}
// 🗑️ Clear all cache
clearCache(): void {
console.log('🧹 Clearing all cache...');
this.cache.clear();
}
// 📊 Cache statistics
getCacheStats() {
const entries = Array.from(this.cache.entries());
const now = Date.now();
return {
totalEntries: entries.length,
validEntries: entries.filter(([, entry]) => now - entry.timestamp <= entry.ttl).length,
expiredEntries: entries.filter(([, entry]) => now - entry.timestamp > entry.ttl).length,
cacheSize: JSON.stringify(Object.fromEntries(this.cache)).length,
};
}
}
// 🎮 Usage with real-time updates
const advancedUsageExample = async (): Promise<void> => {
const baseClient = new UserApiClient({
baseURL: 'https://api.example.com/v1',
apiKey: 'your-api-key-here',
});
const advancedClient = new AdvancedUserApiClient(baseClient);
// 🔔 Set up real-time event listeners
advancedClient.on('userCreated', (user: User) => {
console.log('🎉 New user created:', user.name);
// Update UI, send notifications, etc.
});
advancedClient.on('userUpdated', (user: User) => {
console.log('📝 User updated:', user.name);
// Update UI with new data
});
advancedClient.on('userDeleted', (userId: number) => {
console.log(`🗑️ User ${userId} deleted`);
// Remove from UI, clean up references, etc.
});
try {
// 📋 First call - hits API
const users1 = await advancedClient.listUsers();
console.log('📡 First call - API hit');
// 📋 Second call - hits cache
const users2 = await advancedClient.listUsers();
console.log('💾 Second call - cache hit');
// 👤 Get user with caching
const user = await advancedClient.getUserById(123);
console.log('👤 User (cached):', user.data.name);
// ➕ Create user (invalidates cache)
await advancedClient.createUser({
email: '[email protected]',
name: 'New User',
role: 'user',
});
// 📋 List users again - cache was invalidated, so hits API
const users3 = await advancedClient.listUsers();
console.log('📡 After create - API hit (cache invalidated)');
// 📊 Check cache statistics
const stats = advancedClient.getCacheStats();
console.log('📊 Cache stats:', stats);
} catch (error) {
console.error('💥 Error:', error);
}
};
🤖 Automated Type Generation Workflows
🔄 CI/CD Integration
Let’s create automated workflows to keep our types in sync with API changes:
// 🌟 CI/CD configuration for automated type generation
// .github/workflows/api-types.yml
{
name: 'API Type Generation',
on: {
push: {
branches: ['main', 'develop'],
paths: ['openapi.yaml', 'api-spec/**']
},
pull_request: {
paths: ['openapi.yaml', 'api-spec/**']
},
schedule: [
{ cron: '0 6 * * *' } // Daily at 6 AM
]
},
jobs: {
'generate-types': {
'runs-on': 'ubuntu-latest',
steps: [
{
name: 'Checkout code',
uses: 'actions/checkout@v4'
},
{
name: 'Setup Node.js',
uses: 'actions/setup-node@v4',
with: {
'node-version': '18',
cache: 'npm'
}
},
{
name: 'Install dependencies',
run: 'npm ci'
},
{
name: 'Validate OpenAPI spec',
run: 'npm run validate:openapi'
},
{
name: 'Generate TypeScript types',
run: 'npm run generate:types'
},
{
name: 'Generate API client',
run: 'npm run generate:client'
},
{
name: 'Run type checking',
run: 'npm run typecheck'
},
{
name: 'Run tests',
run: 'npm test'
},
{
name: 'Check for changes',
run: |
if [[ `git status --porcelain` ]]; then
echo "::set-output name=changes::true"
echo "Generated types have changes"
else
echo "::set-output name=changes::false"
echo "No changes in generated types"
fi
id: 'check-changes'
},
{
name: 'Commit generated types',
if: 'steps.check-changes.outputs.changes == \'true\'',
run: |
git config --local user.email "[email protected]"
git config --local user.name "GitHub Action"
git add src/types/
git commit -m "🤖 Update generated API types"
git push
}
]
}
}
}
📋 Build Scripts
Enhanced build scripts for comprehensive type generation:
// 🛠️ Advanced build scripts
// scripts/generate-types.js
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const yaml = require('js-yaml');
class TypeGenerator {
constructor() {
this.config = {
specPath: 'openapi.yaml',
outputDir: 'src/types',
clientDir: 'src/api/generated',
validationEnabled: true,
};
}
// 🔍 Validate OpenAPI specification
validateSpec() {
console.log('🔍 Validating OpenAPI specification...');
try {
const specContent = fs.readFileSync(this.config.specPath, 'utf8');
const spec = yaml.load(specContent);
// Basic validation checks
if (!spec.openapi) {
throw new Error('Missing OpenAPI version');
}
if (!spec.info) {
throw new Error('Missing API info');
}
if (!spec.paths) {
throw new Error('Missing API paths');
}
console.log('✅ OpenAPI specification is valid');
console.log(`📋 API: ${spec.info.title} v${spec.info.version}`);
console.log(`🛣️ Endpoints: ${Object.keys(spec.paths).length}`);
return spec;
} catch (error) {
console.error('❌ OpenAPI validation failed:', error.message);
process.exit(1);
}
}
// 🎯 Generate TypeScript types
generateTypes() {
console.log('🎯 Generating TypeScript types...');
try {
// Ensure output directory exists
if (!fs.existsSync(this.config.outputDir)) {
fs.mkdirSync(this.config.outputDir, { recursive: true });
}
// Generate types using openapi-typescript
execSync(`npx openapi-typescript ${this.config.specPath} --output ${this.config.outputDir}/api.ts`, {
stdio: 'inherit'
});
// Generate additional utility types
this.generateUtilityTypes();
console.log('✅ TypeScript types generated successfully');
} catch (error) {
console.error('❌ Type generation failed:', error.message);
process.exit(1);
}
}
// 🔧 Generate utility types
generateUtilityTypes() {
console.log('🔧 Generating utility types...');
const utilityTypes = `
// 🤖 Auto-generated utility types
// DO NOT EDIT MANUALLY - This file is generated automatically
import type { components, operations } from './api';
// 📦 Schema types
export type User = components['schemas']['User'];
export type CreateUserRequest = components['schemas']['CreateUserRequest'];
export type UpdateUserRequest = components['schemas']['UpdateUserRequest'];
export type ApiResponse<T = any> = components['schemas']['ApiResponse'] & { data?: T };
export type PaginatedResponse<T = any> = components['schemas']['PaginatedResponse'] & { data: T[] };
// 📤 Operation types
export type ListUsersParams = operations['listUsers']['parameters']['query'];
export type ListUsersResponse = operations['listUsers']['responses']['200']['content']['application/json'];
export type CreateUserParams = operations['createUser']['requestBody']['content']['application/json'];
export type CreateUserResponse = operations['createUser']['responses']['201']['content']['application/json'];
export type GetUserParams = operations['getUserById']['parameters']['path'];
export type GetUserResponse = operations['getUserById']['responses']['200']['content']['application/json'];
export type UpdateUserParams = operations['updateUser']['parameters']['path'] & {
body: operations['updateUser']['requestBody']['content']['application/json'];
};
export type UpdateUserResponse = operations['updateUser']['responses']['200']['content']['application/json'];
export type DeleteUserParams = operations['deleteUser']['parameters']['path'];
export type DeleteUserResponse = operations['deleteUser']['responses']['200']['content']['application/json'];
// 🎯 Helper types
export type UserRole = User['role'];
export type UserId = User['id'];
export type UserEmail = User['email'];
// 🔍 Query helpers
export type UserSearchFilters = Pick<ListUsersParams, 'role' | 'isActive'>;
export type UserSortableFields = keyof Pick<User, 'name' | 'email' | 'createdAt' | 'updatedAt'>;
// 📋 Form types
export type UserCreateForm = Omit<CreateUserRequest, 'metadata'> & {
metadata?: Record<string, string>;
};
export type UserUpdateForm = Partial<UserCreateForm>;
// 🛡️ Type guards
export const isValidUserRole = (role: string): role is UserRole => {
return ['admin', 'user', 'moderator'].includes(role);
};
export const isUser = (obj: any): obj is User => {
return obj &&
typeof obj.id === 'number' &&
typeof obj.email === 'string' &&
typeof obj.name === 'string';
};
export const isApiResponse = <T>(obj: any): obj is ApiResponse<T> => {
return obj &&
typeof obj.success === 'boolean' &&
typeof obj.message === 'string';
};
// 📊 Utility functions
export const getUserDisplayName = (user: User): string => {
return user.name || user.email;
};
export const isActiveUser = (user: User): boolean => {
return user.isActive !== false;
};
export const getUserRoleBadgeColor = (role: UserRole): string => {
switch (role) {
case 'admin': return 'red';
case 'moderator': return 'orange';
case 'user': return 'blue';
default: return 'gray';
}
};
// 📈 Constants
export const USER_ROLES = ['admin', 'user', 'moderator'] as const;
export const MAX_USERS_PER_PAGE = 100;
export const DEFAULT_PAGE_SIZE = 20;
// 🎨 Generated at: ${new Date().toISOString()}
`;
fs.writeFileSync(
path.join(this.config.outputDir, 'utils.ts'),
utilityTypes.trim()
);
console.log('✅ Utility types generated');
}
// 🚀 Generate API client
generateClient() {
console.log('🚀 Generating API client...');
try {
// Ensure output directory exists
if (!fs.existsSync(this.config.clientDir)) {
fs.mkdirSync(this.config.clientDir, { recursive: true });
}
// Generate client using openapi-typescript-codegen
execSync(`npx openapi-codegen --input ${this.config.specPath} --output ${this.config.clientDir}`, {
stdio: 'inherit'
});
console.log('✅ API client generated successfully');
} catch (error) {
console.error('❌ Client generation failed:', error.message);
process.exit(1);
}
}
// 🔍 Validate generated types
validateGeneratedTypes() {
console.log('🔍 Validating generated types...');
try {
// Run TypeScript compiler to check for type errors
execSync('npx tsc --noEmit --project tsconfig.json', {
stdio: 'inherit'
});
console.log('✅ Generated types are valid');
} catch (error) {
console.error('❌ Type validation failed');
process.exit(1);
}
}
// 🎯 Run full generation pipeline
async run() {
console.log('🚀 Starting API type generation pipeline...\n');
const startTime = Date.now();
try {
// Step 1: Validate specification
this.validateSpec();
console.log();
// Step 2: Generate TypeScript types
this.generateTypes();
console.log();
// Step 3: Generate API client
this.generateClient();
console.log();
// Step 4: Validate generated code
this.validateGeneratedTypes();
console.log();
const endTime = Date.now();
console.log(`🎉 Type generation completed successfully in ${endTime - startTime}ms`);
} catch (error) {
console.error('💥 Type generation pipeline failed:', error);
process.exit(1);
}
}
}
// 🎮 Run the generator
if (require.main === module) {
const generator = new TypeGenerator();
generator.run();
}
module.exports = TypeGenerator;
🔄 Watch Mode for Development
Development setup with automatic type regeneration:
// 🛠️ Development watch script
// scripts/watch-types.js
const fs = require('fs');
const chokidar = require('chokidar');
const TypeGenerator = require('./generate-types');
class TypeWatcher {
constructor() {
this.generator = new TypeGenerator();
this.isGenerating = false;
this.pendingGeneration = false;
}
// 🔍 Start watching for changes
start() {
console.log('👀 Starting type generation watcher...');
console.log('📁 Watching: openapi.yaml, api-spec/**');
console.log('🔄 Auto-regenerating types on changes...\n');
const watcher = chokidar.watch(['openapi.yaml', 'api-spec/**'], {
persistent: true,
ignoreInitial: false,
});
watcher.on('change', (path) => {
console.log(`📝 File changed: ${path}`);
this.scheduleGeneration();
});
watcher.on('add', (path) => {
console.log(`➕ File added: ${path}`);
this.scheduleGeneration();
});
watcher.on('unlink', (path) => {
console.log(`🗑️ File removed: ${path}`);
this.scheduleGeneration();
});
watcher.on('error', (error) => {
console.error('👀 Watcher error:', error);
});
console.log('✅ Type watcher started. Press Ctrl+C to stop.\n');
// Handle graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 Shutting down type watcher...');
watcher.close();
process.exit(0);
});
}
// ⏰ Schedule type generation with debouncing
scheduleGeneration() {
if (this.isGenerating) {
this.pendingGeneration = true;
console.log('⏳ Generation in progress, queuing...');
return;
}
// Debounce rapid changes
clearTimeout(this.generationTimer);
this.generationTimer = setTimeout(() => {
this.runGeneration();
}, 1000);
}
// 🎯 Run type generation
async runGeneration() {
if (this.isGenerating) return;
this.isGenerating = true;
this.pendingGeneration = false;
try {
console.log('🔄 Regenerating types...');
const startTime = Date.now();
await this.generator.run();
const endTime = Date.now();
console.log(`✅ Types regenerated in ${endTime - startTime}ms\n`);
// Check if another generation is pending
if (this.pendingGeneration) {
console.log('🔄 Running queued generation...');
this.isGenerating = false;
this.scheduleGeneration();
} else {
this.isGenerating = false;
console.log('👀 Watching for changes...\n');
}
} catch (error) {
console.error('❌ Type generation failed:', error);
this.isGenerating = false;
}
}
}
// 🎮 Start the watcher
if (require.main === module) {
const watcher = new TypeWatcher();
watcher.start();
}
module.exports = TypeWatcher;
🧪 Testing Generated Types
🎯 Type-Safe Testing Patterns
Let’s create comprehensive tests for our generated types and API client:
// 🧪 Tests for generated types and API client
// src/__tests__/api-client.test.ts
import { describe, test, expect, beforeEach, jest } from '@jest/globals';
import axios from 'axios';
import { UserApiClient, AdvancedUserApiClient } from '../api/client';
import type {
User,
CreateUserRequest,
UpdateUserRequest,
ListUsersResponse,
CreateUserResponse,
} from '../types/api';
// 🎭 Mock axios
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
describe('UserApiClient', () => {
let client: UserApiClient;
let mockAxiosInstance: jest.Mocked<any>;
beforeEach(() => {
// 🏗️ Setup mock axios instance
mockAxiosInstance = {
request: jest.fn(),
interceptors: {
request: { use: jest.fn() },
response: { use: jest.fn() },
},
};
mockedAxios.create.mockReturnValue(mockAxiosInstance);
client = new UserApiClient({
baseURL: 'https://api.test.com',
apiKey: 'test-key',
});
});
describe('listUsers', () => {
test('should fetch users with correct parameters', async () => {
// 📋 Mock response data
const mockResponse: ListUsersResponse = {
success: true,
message: 'Users retrieved successfully',
data: {
data: [
{
id: 1,
email: '[email protected]',
name: 'Test User',
role: 'user',
isActive: true,
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
},
],
pagination: {
page: 1,
limit: 20,
total: 1,
pages: 1,
},
},
};
mockAxiosInstance.request.mockResolvedValue({ data: mockResponse });
// 🎯 Test the method
const result = await client.listUsers({ page: 1, limit: 20 });
// ✅ Verify the call
expect(mockAxiosInstance.request).toHaveBeenCalledWith({
method: 'GET',
url: '/users',
params: { page: 1, limit: 20 },
});
// ✅ Verify the response
expect(result).toEqual(mockResponse);
expect(result.data.data).toHaveLength(1);
expect(result.data.data[0]).toMatchObject({
id: 1,
email: '[email protected]',
name: 'Test User',
});
});
test('should handle API errors correctly', async () => {
// 💥 Mock error response
const errorResponse = {
response: {
data: { message: 'Validation failed' },
status: 400,
},
};
mockAxiosInstance.request.mockRejectedValue(errorResponse);
// 🎯 Test error handling
await expect(client.listUsers()).rejects.toThrow('API Error: Validation failed');
});
});
describe('createUser', () => {
test('should create user with type-safe data', async () => {
// 👤 Type-safe user data
const userData: CreateUserRequest = {
email: '[email protected]',
name: 'New User',
age: 25,
role: 'user',
metadata: {
department: 'Engineering',
},
};
const mockResponse: CreateUserResponse = {
success: true,
message: 'User created successfully',
data: {
id: 123,
...userData,
isActive: true,
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
},
};
mockAxiosInstance.request.mockResolvedValue({ data: mockResponse });
// 🎯 Test user creation
const result = await client.createUser(userData);
// ✅ Verify the call
expect(mockAxiosInstance.request).toHaveBeenCalledWith({
method: 'POST',
url: '/users',
data: userData,
});
// ✅ Verify the response
expect(result.data.email).toBe(userData.email);
expect(result.data.name).toBe(userData.name);
expect(result.data.id).toBe(123);
});
test('should enforce type safety at compile time', () => {
// 🎯 These should cause TypeScript errors if uncommented:
// ❌ Missing required fields
// const invalidData1: CreateUserRequest = {
// name: 'Test User' // Missing email
// };
// ❌ Invalid role value
// const invalidData2: CreateUserRequest = {
// email: '[email protected]',
// name: 'Test User',
// role: 'invalid-role' // Type error
// };
// ❌ Invalid metadata type
// const invalidData3: CreateUserRequest = {
// email: '[email protected]',
// name: 'Test User',
// metadata: 'invalid' // Should be object
// };
// ✅ Valid data
const validData: CreateUserRequest = {
email: '[email protected]',
name: 'Test User',
role: 'user',
metadata: { key: 'value' },
};
expect(validData.email).toBe('[email protected]');
});
});
});
describe('AdvancedUserApiClient', () => {
let baseClient: UserApiClient;
let advancedClient: AdvancedUserApiClient;
beforeEach(() => {
baseClient = new UserApiClient({
baseURL: 'https://api.test.com',
apiKey: 'test-key',
});
advancedClient = new AdvancedUserApiClient(baseClient);
});
describe('caching', () => {
test('should cache API responses', async () => {
// 📋 Mock response
const mockUser: User = {
id: 1,
email: '[email protected]',
name: 'Cached User',
role: 'user',
isActive: true,
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
};
const mockResponse = {
success: true,
message: 'User retrieved',
data: mockUser,
};
// 🎭 Mock base client method
jest.spyOn(baseClient, 'getUserById').mockResolvedValue(mockResponse);
// 🎯 First call - should hit API
const result1 = await advancedClient.getUserById(1);
expect(baseClient.getUserById).toHaveBeenCalledTimes(1);
// 🎯 Second call - should use cache
const result2 = await advancedClient.getUserById(1);
expect(baseClient.getUserById).toHaveBeenCalledTimes(1); // Still only called once
expect(result1).toEqual(result2);
});
test('should invalidate cache on user update', async () => {
// 🎭 Mock methods
jest.spyOn(baseClient, 'getUserById').mockResolvedValue({
success: true,
message: 'User retrieved',
data: { id: 1, email: '[email protected]', name: 'Test' } as User,
});
jest.spyOn(baseClient, 'updateUser').mockResolvedValue({
success: true,
message: 'User updated',
data: { id: 1, email: '[email protected]', name: 'Updated' } as User,
});
// 🎯 Get user - caches result
await advancedClient.getUserById(1);
expect(baseClient.getUserById).toHaveBeenCalledTimes(1);
// 🎯 Update user - should invalidate cache
await advancedClient.updateUser(1, { name: 'Updated Name' });
// 🎯 Get user again - should hit API due to cache invalidation
await advancedClient.getUserById(1);
expect(baseClient.getUserById).toHaveBeenCalledTimes(2);
});
});
describe('real-time events', () => {
test('should emit events on user operations', async () => {
// 🎭 Mock event listeners
const userCreatedListener = jest.fn();
const userUpdatedListener = jest.fn();
const userDeletedListener = jest.fn();
advancedClient.on('userCreated', userCreatedListener);
advancedClient.on('userUpdated', userUpdatedListener);
advancedClient.on('userDeleted', userDeletedListener);
// 🎭 Mock base client methods
const mockUser: User = {
id: 123,
email: '[email protected]',
name: 'Event User',
role: 'user',
isActive: true,
createdAt: '2023-01-01T00:00:00Z',
updatedAt: '2023-01-01T00:00:00Z',
};
jest.spyOn(baseClient, 'createUser').mockResolvedValue({
success: true,
message: 'User created',
data: mockUser,
});
jest.spyOn(baseClient, 'updateUser').mockResolvedValue({
success: true,
message: 'User updated',
data: { ...mockUser, name: 'Updated User' },
});
jest.spyOn(baseClient, 'deleteUser').mockResolvedValue({
success: true,
message: 'User deleted',
});
// 🎯 Test create event
await advancedClient.createUser({
email: '[email protected]',
name: 'Event User',
});
expect(userCreatedListener).toHaveBeenCalledWith(mockUser);
// 🎯 Test update event
await advancedClient.updateUser(123, { name: 'Updated User' });
expect(userUpdatedListener).toHaveBeenCalledWith({
...mockUser,
name: 'Updated User',
});
// 🎯 Test delete event
await advancedClient.deleteUser(123);
expect(userDeletedListener).toHaveBeenCalledWith(123);
});
});
});
// 🎯 Type-level tests using TypeScript's type system
describe('Type Safety', () => {
test('should ensure correct types are used', () => {
// ✅ These demonstrate compile-time type safety
// User type should have all required properties
const user: User = {
id: 1,
email: '[email protected]',
name: 'Test User',
// Optional properties can be omitted
};
// CreateUserRequest should not include generated fields
const createRequest: CreateUserRequest = {
email: '[email protected]',
name: 'New User',
// id, createdAt, updatedAt should not be allowed here
};
// UpdateUserRequest should make all fields optional
const updateRequest: UpdateUserRequest = {
name: 'Updated Name',
// All other fields are optional
};
expect(user.id).toBe(1);
expect(createRequest.email).toBe('[email protected]');
expect(updateRequest.name).toBe('Updated Name');
});
});
🎉 Conclusion
Congratulations! You’ve mastered the art of generating TypeScript types from OpenAPI specifications! 🔧
🎯 What You’ve Learned
- 📋 OpenAPI Specifications: Creating comprehensive API documentation as code
- 🤖 Automated Type Generation: Tools and workflows for type synchronization
- 🏗️ Type-Safe API Clients: Building robust HTTP clients with full type safety
- ⚡ Advanced Features: Caching, deduplication, and real-time updates
- 🔄 CI/CD Integration: Automated workflows for continuous type synchronization
- 🧪 Testing Strategies: Comprehensive testing for generated types and clients
🚀 Key Benefits
- 🛡️ Type Safety: Eliminate runtime errors with compile-time type checking
- 🔄 Synchronization: Keep frontend and backend types automatically in sync
- 📚 Documentation: Living documentation that’s always up-to-date
- ⚡ Developer Experience: IntelliSense, autocomplete, and instant feedback
- 🧹 Maintainability: Reduce manual type maintenance and human errors
🔥 Best Practices Recap
- 📋 Single Source of Truth: Use OpenAPI specs as the authoritative API contract
- 🤖 Automation: Automate type generation in development and CI/CD pipelines
- 🧪 Validation: Always validate generated types with TypeScript compiler
- 📦 Versioning: Version your API specs and generated types together
- 🔍 Monitoring: Set up alerts for type generation failures and schema changes
You’re now equipped to build enterprise-grade, type-safe REST API integrations that scale with your applications and stay perfectly synchronized with your backend services! 🌟
Happy coding, and may your APIs always be perfectly typed! 🎯✨