The design pattern below is Template Method Pattern used in combination with the Abstract Class Pattern. It is a common practice in web development to create an Abstract API Class that defines a general structure for handling API requests and then extend this abstract class for each specific entity. Here's how you can implement this same pattern in TypeScript:
// Abstract API Class (Template Method Pattern)
abstract class AbstractApi {
protected baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
// Generic request method
protected async request<T>(
endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
data?: any
): Promise<T> {
const options: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
},
body: data ? JSON.stringify(data) : undefined,
};
try {
const response = await fetch(`${this.baseUrl}/${endpoint}`, options);
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return (await response.json()) as T;
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
// CRUD methods
protected get<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, 'GET');
}
protected post<T>(endpoint: string, data: any): Promise<T> {
return this.request<T>(endpoint, 'POST', data);
}
protected put<T>(endpoint: string, data: any): Promise<T> {
return this.request<T>(endpoint, 'PUT', data);
}
protected delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, 'DELETE');
}
}
// User API Class (Concrete Implementation)
interface User {
id: number;
name: string;
email: string;
}
class UserApi extends AbstractApi {
constructor() {
super('/api/users');
}
// Entity-specific methods
public getUserById(userId: number): Promise<User> {
return this.get<User>(`${userId}`);
}
public createUser(userData: Partial<User>): Promise<User> {
return this.post<User>('', userData);
}
public updateUser(userId: number, userData: Partial<User>): Promise<User> {
return this.put<User>(`${userId}`, userData);
}
public deleteUser(userId: number): Promise<void> {
return this.delete<void>(`${userId}`);
}
}
// Example usage
const userApi = new UserApi();
userApi.getUserById(1).then((user) => console.log(user));- Defined as an
abstract classto ensure it cannot be instantiated directly. - Contains a generic
requestmethod for handling API requests, utilizing TypeScript generics (<T>) for typed responses. - Provides reusable CRUD methods (
get,post,put,delete) with TypeScript typing for safer API calls.
- Extends the abstract class and implements specific methods tailored for the "User" entity.
- Uses TypeScript interfaces (e.g.,
User) to define expected data structures.
- Generics (
<T>): Allow therequestmethod to return different types depending on the API call. - Abstract Classes: Enforces extending classes to implement custom behavior without direct instantiation.
- Interfaces (
User): Defines the structure of the expected API response, making the code type-safe.
- Type Safety: Errors are caught at compile time due to strict typing.
- Reusability: Common API logic is centralized in the abstract class, reducing duplication.
- Scalability: Easily extendable for additional entities (e.g.,
ProductApi,OrderApi).
This approach improves maintainability and ensures type-safe API interactions in TypeScript applications.