Back to Notes
Tutorial
Mastering API Integration with TypeScript
August 17, 2023
With the power of TypeScript, we can craft an elegant API service that not only simplifies our fetch calls but also provides robust error handling.
The Need for an Elegant API Service
As our applications grow in complexity, so does the need for efficient and organized API calls. By centralizing our API logic, we can ensure consistency, reduce redundancy, and make our codebase more maintainable.
Diving into the Code
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 Explained
TypeScript Types: By leveraging TypeScript's static typing, we can ensure that our API responses and errors conform to expected structures. This reduces runtime errors and enhances code readability.
HTTP Status Codes: With the
HttpStatusCode
enumeration, we can handle different response statuses more descriptively, making our error handling more intuitive.Base URL and Headers: Centralizing these configurations ensures that every API call we make adheres to the same standards.
Dynamic URL Construction: The
url
function allows us to dynamically construct our endpoint URLs based on the providedid
.Centralized Fetch Function: The
fetchFromApi
function acts as the heart of our service. It handles the actual fetch call, checks the response status, and either returns the data or throws an appropriate error.CRUD Operations: The
getData
,updateData
,postData
, anddeleteData
functions are specialized functions for different CRUD operations. They utilize the centralizedfetchFromApi
function, ensuring consistent behavior across all API calls.
Benefits of This Approach
Consistency: By centralizing our API logic, every fetch call we make adheres to the same standards, ensuring consistent behaviour across our application.
Enhanced Error Handling: With centralized error handling, we can provide more descriptive error messages based on the HTTP status code, improving the user experience.
Scalability: As our application grows, we can easily extend our API service by adding more utility functions or enhancing existing ones.
Conclusion
Crafting an elegant API service for front-end applications is all about centralization, consistency, and leveraging the power of TypeScript. By following the principles outlined in this guide, developers can ensure a more maintainable, scalable, and user-friendly application.
The Need for an Elegant API Service
As our applications grow in complexity, so does the need for efficient and organized API calls. By centralizing our API logic, we can ensure consistency, reduce redundancy, and make our codebase more maintainable.
Diving into the Code
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 Explained
TypeScript Types: By leveraging TypeScript's static typing, we can ensure that our API responses and errors conform to expected structures. This reduces runtime errors and enhances code readability.
HTTP Status Codes: With the
HttpStatusCode
enumeration, we can handle different response statuses more descriptively, making our error handling more intuitive.Base URL and Headers: Centralizing these configurations ensures that every API call we make adheres to the same standards.
Dynamic URL Construction: The
url
function allows us to dynamically construct our endpoint URLs based on the providedid
.Centralized Fetch Function: The
fetchFromApi
function acts as the heart of our service. It handles the actual fetch call, checks the response status, and either returns the data or throws an appropriate error.CRUD Operations: The
getData
,updateData
,postData
, anddeleteData
functions are specialized functions for different CRUD operations. They utilize the centralizedfetchFromApi
function, ensuring consistent behavior across all API calls.
Benefits of This Approach
Consistency: By centralizing our API logic, every fetch call we make adheres to the same standards, ensuring consistent behaviour across our application.
Enhanced Error Handling: With centralized error handling, we can provide more descriptive error messages based on the HTTP status code, improving the user experience.
Scalability: As our application grows, we can easily extend our API service by adding more utility functions or enhancing existing ones.
Conclusion
Crafting an elegant API service for front-end applications is all about centralization, consistency, and leveraging the power of TypeScript. By following the principles outlined in this guide, developers can ensure a more maintainable, scalable, and user-friendly application.
The Need for an Elegant API Service
As our applications grow in complexity, so does the need for efficient and organized API calls. By centralizing our API logic, we can ensure consistency, reduce redundancy, and make our codebase more maintainable.
Diving into the Code
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 Explained
TypeScript Types: By leveraging TypeScript's static typing, we can ensure that our API responses and errors conform to expected structures. This reduces runtime errors and enhances code readability.
HTTP Status Codes: With the
HttpStatusCode
enumeration, we can handle different response statuses more descriptively, making our error handling more intuitive.Base URL and Headers: Centralizing these configurations ensures that every API call we make adheres to the same standards.
Dynamic URL Construction: The
url
function allows us to dynamically construct our endpoint URLs based on the providedid
.Centralized Fetch Function: The
fetchFromApi
function acts as the heart of our service. It handles the actual fetch call, checks the response status, and either returns the data or throws an appropriate error.CRUD Operations: The
getData
,updateData
,postData
, anddeleteData
functions are specialized functions for different CRUD operations. They utilize the centralizedfetchFromApi
function, ensuring consistent behavior across all API calls.
Benefits of This Approach
Consistency: By centralizing our API logic, every fetch call we make adheres to the same standards, ensuring consistent behaviour across our application.
Enhanced Error Handling: With centralized error handling, we can provide more descriptive error messages based on the HTTP status code, improving the user experience.
Scalability: As our application grows, we can easily extend our API service by adding more utility functions or enhancing existing ones.
Conclusion
Crafting an elegant API service for front-end applications is all about centralization, consistency, and leveraging the power of TypeScript. By following the principles outlined in this guide, developers can ensure a more maintainable, scalable, and user-friendly application.
The Need for an Elegant API Service
As our applications grow in complexity, so does the need for efficient and organized API calls. By centralizing our API logic, we can ensure consistency, reduce redundancy, and make our codebase more maintainable.
Diving into the Code
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 Explained
TypeScript Types: By leveraging TypeScript's static typing, we can ensure that our API responses and errors conform to expected structures. This reduces runtime errors and enhances code readability.
HTTP Status Codes: With the
HttpStatusCode
enumeration, we can handle different response statuses more descriptively, making our error handling more intuitive.Base URL and Headers: Centralizing these configurations ensures that every API call we make adheres to the same standards.
Dynamic URL Construction: The
url
function allows us to dynamically construct our endpoint URLs based on the providedid
.Centralized Fetch Function: The
fetchFromApi
function acts as the heart of our service. It handles the actual fetch call, checks the response status, and either returns the data or throws an appropriate error.CRUD Operations: The
getData
,updateData
,postData
, anddeleteData
functions are specialized functions for different CRUD operations. They utilize the centralizedfetchFromApi
function, ensuring consistent behavior across all API calls.
Benefits of This Approach
Consistency: By centralizing our API logic, every fetch call we make adheres to the same standards, ensuring consistent behaviour across our application.
Enhanced Error Handling: With centralized error handling, we can provide more descriptive error messages based on the HTTP status code, improving the user experience.
Scalability: As our application grows, we can easily extend our API service by adding more utility functions or enhancing existing ones.
Conclusion
Crafting an elegant API service for front-end applications is all about centralization, consistency, and leveraging the power of TypeScript. By following the principles outlined in this guide, developers can ensure a more maintainable, scalable, and user-friendly application.