_private/qwestly-docs/Engineering/API-Request-Client.md

API Request Client Documentation

This is a TypeScript utility that provides a clean, type-safe interface for making HTTP requests to your API endpoints. It includes built-in error handling, response typing, request body typing, and configuration options.

Features

  • Type-safe request and response handling with dual generic support
  • Consistent error handling across all requests
  • Support for JSON and FormData payloads
  • Configurable base URL
  • Built-in TypeScript generics for both response and request body typing
  • Automatic Content-Type header management

Installation

The API client is available as part of the core utilities. Import it directly from the utils directory:

import ApiRequest from '@/lib/api-request';

Basic Usage

Generic Type Parameters

The API client supports two generic type parameters:

  • TResponse: Type for the response data
  • TRequest: Type for the request body (for POST/PUT/PATCH methods)
// GET requests: ApiRequest.get<TResponse>(path)
const { data, error } = await ApiRequest.get<User>('users/123');

// POST/PUT/PATCH requests: ApiRequest.method<TResponse, TRequest>(path, data)
const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', userData);

GET Request

// GET: /api/users
const { data, error } = await ApiRequest.get('users');

// With type definition
interface User {
  id: string;
  name: string;
  email: string;
}

const { data, error } = await ApiRequest.get<User>('users/123');
if (error) {
  console.error('Failed to fetch user:', error);
  return;
}
console.log('User data:', data); // data is typed as User | null

POST Request

// POST: /api/users
interface CreateUserRequest {
  name: string;
  email: string;
  role: 'admin' | 'user';
}

interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
  createdAt: string;
}

const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', {
  name: 'John Doe',
  email: 'john@example.com',
  role: 'user'
});

// With FormData
const formData = new FormData();
formData.append('file', file);
const { data, error } = await ApiRequest.post<UploadResponse, FormData>('upload', formData);

Other HTTP Methods

// PATCH: /api/users/123
interface UpdateUserRequest {
  name?: string;
  email?: string;
}

const { data, error } = await ApiRequest.patch<User, UpdateUserRequest>('users/123', {
  name: 'Updated Name'
});

// PUT: /api/users/456
interface UpdateResourceRequest {
  status: 'active' | 'inactive';
}

const { data, error } = await ApiRequest.put<Resource, UpdateResourceRequest>('resources/456', {
  status: 'active'
});

// DELETE: /api/users/123
const { data, error } = await ApiRequest.delete('users/123');

Configuration

You can configure the base URL:

ApiRequest.configure({
  baseURL: '/custom-api'
});

Type Definitions

APIResponse

interface APIResponse<TResponse = any> {
  data: TResponse | null;
  error: APIError | null;
}

APIError

interface APIError extends Error {
  status?: number;  // HTTP status code
  data?: unknown;   // Error response data from server
}

APIConfig

interface APIConfig {
  baseURL: string;  // Base URL for all API requests
}

Error Handling

The library provides consistent error handling across all requests:

const { data, error } = await ApiRequest.get<User>('users/123');
if (error) {
  if (error.status === 404) {
    console.error('User not found');
  } else if (error.status === 403) {
    console.error('Permission denied');
  } else {
    console.error('An error occurred:', error.message);
  }
  // Access additional error data if available
  if (error.data) {
    console.error('Server error details:', error.data);
  }
  return;
}

Content-Type Handling

  • JSON requests automatically include Content-Type: application/json
  • FormData requests automatically omit Content-Type to allow the browser to set the correct multipart boundary
  • All responses are automatically parsed as JSON

Best Practices

  1. Always Type Your Responses and Request Bodies

    // ❌ Avoid - No type safety
    const { data } = await ApiRequest.get('users');
    const { data } = await ApiRequest.post('users', userData);
    
    // ✅ Better - Full type safety
    const { data } = await ApiRequest.get<User[]>('users');
    const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', userData);
    
  2. Always Handle Errors

    // ❌ Avoid
    const { data } = await ApiRequest.post<User, CreateUserRequest>('users', userData);
    
    // ✅ Better
    const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', userData);
    if (error) {
      // Handle error appropriately
      return;
    }
    
  3. Use Type Definitions for Request Bodies

    interface CreateUserRequest {
      name: string;
      email: string;
      role: 'admin' | 'user';
    }
    
    const { data, error } = await ApiRequest.post<User, CreateUserRequest>('users', {
      name: 'John Doe',
      email: 'john@example.com',
      role: 'user'
    });
    
  4. Define Clear Interfaces for Both Request and Response

    // Request interface
    interface UpdateCandidateRequest {
      name?: string;
      email?: string;
      linkedin_user_name?: string;
    }
    
    // Response interface
    interface Candidate {
      id: string;
      name: string;
      email: string;
      linkedin_user_name?: string;
      created_at: string;
    }
    
    // Full type safety
    const { data, error } = await ApiRequest.patch<Candidate, UpdateCandidateRequest>(
      `enhanced/candidates/${userId}`,
      updateData
    );
    

Error Logging

All API errors are automatically logged to the console with the following information:

  • HTTP method
  • Request path
  • Error details

This helps with debugging and monitoring API issues in development.

Notes

  • All methods return a Promise with an APIResponse object
  • The base URL defaults to /api but can be configured
  • Request URLs are automatically prefixed with the base URL
  • Error responses include both the error message and the original response data when available
  • Required: All API request methods MUST include generic type parameters for full type safety