js

TypeScript API Clients: Build Type-Safe Apps with OpenAPI Generator and Custom Axios Interceptors

Learn to build type-safe API clients using OpenAPI Generator and custom Axios interceptors in TypeScript. Master error handling, authentication, and testing for robust applications.

TypeScript API Clients: Build Type-Safe Apps with OpenAPI Generator and Custom Axios Interceptors

I’ve been thinking about API clients lately because I’ve spent too many hours debugging type mismatches between frontend and backend systems. Remember those moments when you thought you were sending the right data, only to get a 400 error because of a simple type mismatch? That frustration led me to build type-safe API clients that actually work.

What if your development environment could catch API integration issues before they reach production?

Let me show you how OpenAPI Generator creates type-safe clients from your API specifications. This approach ensures your client code always matches your backend contracts. Here’s a basic setup:

openapi-generator-cli generate \
  -i api-spec.yaml \
  -g typescript-axios \
  -o src/generated \
  --additional-properties=useSingleRequestParameter=true

The generated client gives you fully typed methods for every endpoint. But raw generated clients often lack the practical features needed for real applications. That’s where custom Axios interceptors come into play.

Have you ever needed to add authentication tokens to every request automatically?

Let me demonstrate request interceptors that handle authentication seamlessly:

import { AxiosInstance, InternalAxiosRequestConfig } from 'axios';

export class AuthInterceptor {
  constructor(private axiosInstance: AxiosInstance) {}

  setup() {
    this.axiosInstance.interceptors.request.use(
      (config: InternalAxiosRequestConfig) => {
        const token = localStorage.getItem('authToken');
        if (token && config.headers) {
          config.headers.Authorization = `Bearer ${token}`;
        }
        return config;
      },
      (error) => Promise.reject(error)
    );
  }
}

Response interceptors handle errors consistently across your entire application. How many times have you written the same error handling logic across different API calls?

export class ErrorInterceptor {
  constructor(private axiosInstance: AxiosInstance) {}

  setup() {
    this.axiosInstance.interceptors.response.use(
      (response) => response,
      (error) => {
        if (error.response?.status === 401) {
          window.location.href = '/login';
          return Promise.reject(new Error('Authentication required'));
        }
        
        if (error.response?.status === 429) {
          return this.handleRateLimit(error);
        }
        
        return Promise.reject(this.normalizeError(error));
      }
    );
  }
  
  private handleRateLimit(error: any) {
    const retryAfter = error.response.headers['retry-after'];
    console.warn(`Rate limited. Retry after: ${retryAfter} seconds`);
    return Promise.reject(error);
  }
}

Retry logic transforms unreliable API calls into robust operations. Consider network flakes or temporary server issues - wouldn’t it be better if your client handled these automatically?

export class RetryInterceptor {
  private maxRetries = 3;
  private retryDelay = 1000;

  setup() {
    this.axiosInstance.interceptors.response.use(
      (response) => response,
      async (error) => {
        const config = error.config;
        
        if (!this.shouldRetry(error) || config.retryCount >= this.maxRetries) {
          return Promise.reject(error);
        }

        config.retryCount = config.retryCount || 0;
        config.retryCount += 1;

        await this.delay(this.retryDelay * config.retryCount);
        
        return this.axiosInstance(config);
      }
    );
  }

  private shouldRetry(error: any): boolean {
    return error.code === 'ECONNABORTED' || 
           error.response?.status >= 500;
  }
}

Caching interceptors dramatically improve performance by reducing redundant network calls. Why fetch the same data multiple times when you can serve it from memory?

export class CacheInterceptor {
  private cache = new Map<string, { data: any; timestamp: number }>();
  private cacheTTL = 5 * 60 * 1000; // 5 minutes

  setup() {
    this.axiosInstance.interceptors.request.use(
      (config) => {
        if (config.method?.toLowerCase() === 'get' && config.cacheKey) {
          const cached = this.cache.get(config.cacheKey);
          if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
            return Promise.resolve({
              ...config,
              adapter: () => Promise.resolve({
                data: cached.data,
                status: 200,
                statusText: 'OK',
                headers: {},
                config
              })
            });
          }
        }
        return config;
      }
    );
  }
}

Putting everything together creates a client that’s both type-safe and production-ready:

import { Configuration, TasksApi } from './generated';
import { 
  AuthInterceptor, 
  ErrorInterceptor, 
  RetryInterceptor,
  CacheInterceptor 
} from './interceptors';

export class ApiClient {
  private tasksApi: TasksApi;

  constructor(basePath: string) {
    const config = new Configuration({ basePath });
    const axiosInstance = new AxiosInstance(config);
    
    this.setupInterceptors(axiosInstance);
    this.tasksApi = new TasksApi(config, undefined, axiosInstance);
  }

  private setupInterceptors(instance: AxiosInstance) {
    new AuthInterceptor(instance).setup();
    new ErrorInterceptor(instance).setup();
    new RetryInterceptor(instance).setup();
    new CacheInterceptor(instance).setup();
  }

  async createTask(title: string, description?: string) {
    // TypeScript will enforce the correct parameter types
    return this.tasksApi.createTask({ 
      title, 
      description,
      priority: 'medium' 
    });
  }
}

The beauty of this approach becomes apparent when your API evolves. When the backend team adds new fields or changes requirements, your client code immediately shows type errors where updates are needed. No more silent failures or runtime surprises.

Doesn’t it feel reassuring to know that your IDE will alert you about breaking changes before they cause production issues?

This combination of automated type generation and customizable interceptors creates API clients that are both safe and practical. The types prevent integration errors while the interceptors handle the cross-cutting concerns that make applications robust.

I’d love to hear about your experiences with API client development. What challenges have you faced, and how did you solve them? If this approach resonates with you, please share it with your team and let me know how it works in your projects. Your feedback helps improve these solutions for everyone.

Keywords: TypeScript API client, OpenAPI Generator, Axios interceptors, type-safe API, REST API client, TypeScript OpenAPI, API code generation, HTTP client TypeScript, OpenAPI TypeScript generator, custom Axios interceptors



Similar Posts
Blog Image
Build Type-Safe GraphQL APIs with NestJS, Prisma, and Code-First Development: Complete Guide

Learn to build type-safe GraphQL APIs using NestJS, Prisma & code-first development. Master authentication, performance optimization & production deployment.

Blog Image
Complete Guide to Next.js Prisma Integration: Build Type-Safe Full-Stack Applications in 2024

Learn how to integrate Next.js with Prisma ORM for type-safe full-stack applications. Master database operations, schema management, and seamless API development.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM for Type-Safe Full-Stack Development

Learn how to integrate Next.js with Prisma ORM for type-safe full-stack development. Complete guide with setup, API routes, and database operations.

Blog Image
Next.js Prisma Integration Guide: Build Type-Safe Full-Stack Applications with Modern Database Toolkit

Learn how to integrate Next.js with Prisma for full-stack development. Build type-safe applications with seamless database operations and SSR capabilities.

Blog Image
Complete Guide to Next.js Prisma Integration: Build Type-Safe Full-Stack Apps in 2024

Learn how to integrate Next.js with Prisma for powerful full-stack development. Build type-safe, scalable applications with seamless database operations.

Blog Image
Build Production-Ready GraphQL APIs with NestJS, Prisma and Redis: Complete Tutorial 2024

Build scalable GraphQL APIs with NestJS, Prisma & Redis. Learn authentication, real-time subscriptions, caching, testing & Docker deployment. Complete production guide.