TYPESCRIPT Published: 08.2023
Views: 86

>_ Mastering API Integration with TypeScript

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

  1. Type Safety

    • Compile-time checking
    • Predictable responses
    • Better maintainability
  2. Centralized Logic

    • Consistent error handling
    • Standardized requests
    • Easy maintenance
  3. Enhanced Developer Experience

    • Clear type definitions
    • Intuitive API
    • Better IDE support

Best Practices

  1. Use proper type annotations
  2. Implement comprehensive error handling
  3. Maintain consistent response formats
  4. Document API behavior
  5. Handle edge cases properly

Remember that a well-structured API service is fundamental to a maintainable application.

TAGS:
TUTORIAL SCRIPTS ARCHITECTURE