Back to Notes

Tutorial

Mastering API Integration with TypeScript

August 17, 2023

0min read

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 provided id.

  • 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, and deleteData functions are specialized functions for different CRUD operations. They utilize the centralized fetchFromApi 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 provided id.

  • 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, and deleteData functions are specialized functions for different CRUD operations. They utilize the centralized fetchFromApi 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 provided id.

  • 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, and deleteData functions are specialized functions for different CRUD operations. They utilize the centralized fetchFromApi 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 provided id.

  • 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, and deleteData functions are specialized functions for different CRUD operations. They utilize the centralized fetchFromApi 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.

Get in touch

Seeking a fresh opportunity or have an inquiry? Don't hesitate to reach out to me.

Get in touch

Seeking a fresh opportunity or have an inquiry? Don't hesitate to reach out to me.

Get in touch

Seeking a fresh opportunity or have an inquiry? Don't hesitate to reach out to me.

©

2024

Dylan Britz