TYPESCRIPT
Published: 08.2023
Views: 86
>_ Mastering API Integration with TypeScript
Build a robust and type-safe API service layer using TypeScript. Learn how to create an elegant solution for handling API calls with proper error handling and response typing.
Building a Robust API Service with TypeScript
Let's explore how to create an elegant and type-safe API service that simplifies fetch calls and provides comprehensive error handling.
Core Implementation
import type { ApiError, ApiResponse, User } from '../types';
import { HttpStatusCode } from './HttpStatusCode';
const base_url = import.meta.env.VITE_API_URL;
const header = {
Accept: 'application/vnd.api+json',
'Content-Type': 'application/vnd.api+json',
};
const url = async (params: string) => {
return `${base_url}/${params}`;
};
const token = () => {
return import.meta.env.VITE_API_TOKEN; // or some other secure function to get your token
};
const fetchFromApi = async <T>(
url: string,
options: RequestInit
): Promise<ApiResponse<T> | ApiError> => {
try {
const response = await fetch(url, options);
if (!response.ok) {
switch (response.status) {
case HttpStatusCode.UNAUTHORIZED:
throw new Error('Invalid API key');
case HttpStatusCode.NOT_FOUND:
throw new Error('Endpoint not found');
default:
throw new Error('An error occurred while fetching data');
}
}
return await response.json();
} catch (error: any) {
console.error('Error in fetchFromApi function: ', error);
return {
status: 'error',
message: error?.message ?? 'An error occurred while fetching data',
} as ApiError;
}
};
export const getData = async <T>(
endPoint: string
): Promise<ApiResponse<T> | ApiError> => {
return await fetchFromApi(await url(endPoint), {
headers: {
...header,
Authorization: `Bearer ${await token()}`,
},
method: 'GET',
});
};
export const updateData = async <T>(
endPoint: string,
data: any
): Promise<ApiResponse<T> | ApiError> => {
return await fetchFromApi(await url(endPoint), {
method: 'PATCH',
headers: {
...header,
Authorization: `Bearer ${await token()}`,
},
body: JSON.stringify({ data: { ...data } }),
});
};
export const postData = async <T>(
endPoint: string,
data: any
): Promise<ApiResponse<T> | ApiError> => {
return await fetchFromApi(await url(endPoint), {
method: 'POST',
headers: {
...header,
Authorization: `Bearer ${await token()}`,
},
body: JSON.stringify({ data: { ...data } }),
});
};
export const deleteData = async (
endPoint: string
): Promise<ApiResponse<null> | ApiError> => {
return await fetchFromApi(await url(endPoint), {
method: 'DELETE',
headers: {
...header,
Authorization: `Bearer ${await token()}`,
},
});
};
Key Components
Type Definitions
- ApiError interface
- ApiResponse generic type
- HTTP status codes enum
Configuration
- Base URL management
- Header standardization
- Authentication handling
Fetch Wrapper
- Type-safe responses
- Error handling
- Request configuration
CRUD Operations
GET Requests
// GET Request Implementation
const getData = async <T>(endPoint: string): Promise<ApiResponse<T> | ApiError> => {
return await fetchFromApi(await url(endPoint), {
headers: {
...header,
Authorization: `Bearer ${await token()}`,
},
method: 'GET',
});
};
// Example usage:
const response = await getData<User>('users/1');
if ('status' in response && response.status === 'error') {
console.error(response.message);
} else {
console.log(response.data);
}
POST Requests
// POST Request Implementation
const postData = async <T>(
endPoint: string,
data: any
): Promise<ApiResponse<T> | ApiError> => {
return await fetchFromApi(await url(endPoint), {
method: 'POST',
headers: {
...header,
Authorization: `Bearer ${await token()}`,
},
body: JSON.stringify({ data: { ...data } }),
});
};
// Example usage:
const newUser = { name: 'John Doe', email: '[email protected]' };
const response = await postData<User>('users', newUser);
UPDATE Requests
// UPDATE Request Implementation
const updateData = async <T>(
endPoint: string,
data: any
): Promise<ApiResponse<T> | ApiError> => {
return await fetchFromApi(await url(endPoint), {
method: 'PATCH',
headers: {
...header,
Authorization: `Bearer ${await token()}`,
},
body: JSON.stringify({ data: { ...data } }),
});
};
// Example usage:
const userUpdate = { name: 'John Updated' };
const response = await updateData<User>('users/1', userUpdate);
DELETE Requests
// DELETE Request Implementation
const deleteData = async (
endPoint: string
): Promise<ApiResponse<null> | ApiError> => {
return await fetchFromApi(await url(endPoint), {
method: 'DELETE',
headers: {
...header,
Authorization: `Bearer ${await token()}`,
},
});
};
// Example usage:
const response = await deleteData('users/1');
if ('status' in response && response.status === 'error') {
console.error('Failed to delete user');
} else {
console.log('User deleted successfully');
}
Benefits
-
Type Safety
- Compile-time checking
- Predictable responses
- Better maintainability
-
Centralized Logic
- Consistent error handling
- Standardized requests
- Easy maintenance
-
Enhanced Developer Experience
- Clear type definitions
- Intuitive API
- Better IDE support
Best Practices
- Use proper type annotations
- Implement comprehensive error handling
- Maintain consistent response formats
- Document API behavior
- Handle edge cases properly
Remember that a well-structured API service is fundamental to a maintainable application.
TAGS:
TUTORIAL
SCRIPTS
ARCHITECTURE