Organizing Centralized API Endpoints with Axios

Centralizing API endpoints when using Axios is essential for building scalable, maintainable, and robust web applications. This article explores the most effective architectural patterns for organizing Axios requests, ranging from base client instances to domain-specific services and repository patterns. Implementing these strategies simplifies error handling, authentication token management, and URL updates across your entire codebase.


1. The Base Axios Instance Pattern

The foundation of any centralized API architecture is a pre-configured Axios instance. Instead of importing the raw axios package in multiple files, you create a shared instance with default configurations like base URLs, timeouts, and headers.

// src/api/client.js
import axios from 'axios';

const apiClient = axios.create({
  baseURL: process.env.API_BASE_URL || 'https://api.example.com/v1',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Centralized request interceptor (e.g., attaching auth tokens)
apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// Centralized response interceptor (e.g., global error handling)
apiClient.interceptors.response.use(
  (response) => response.data,
  (error) => {
    // Handle 401 Unauthorized, logging, etc.
    return Promise.reject(error);
  }
);

export default apiClient;

2. The Module / Service-Based Pattern

The service-based pattern groups related API calls by feature or domain (e.g., auth, users, products). Each module exports specific functions that encapsulate endpoint paths, query parameters, and payload formatting.

src/api/
├── client.js
├── authService.js
├── userService.js
└── index.js

Implementation Example:

// src/api/userService.js
import apiClient from './client';

export const userService = {
  getAll: (params) => apiClient.get('/users', { params }),
  getById: (id) => apiClient.get(`/users/${id}`),
  create: (data) => apiClient.post('/users', data),
  update: (id, data) => apiClient.put(`/users/${id}`, data),
  delete: (id) => apiClient.delete(`/users/${id}`),
};
// src/api/index.js
export { userService } from './userService';
export { authService } from './authService';

Benefits:

  • Clear separation of concerns.
  • Easy to mock during unit testing.
  • Component code remains clean without hardcoded URLs.

3. The Generic Repository Pattern

For applications interacting with standardized RESTful APIs, the Repository Pattern abstracts common CRUD operations into a reusable base class. Specific resources extend this base class to inherit standard operations and add custom endpoints.

// src/api/BaseRepository.js
import apiClient from './client';

export class BaseRepository {
  constructor(resource) {
    this.resource = resource;
  }

  getAll(params) {
    return apiClient.get(`/${this.resource}`, { params });
  }

  get(id) {
    return apiClient.get(`/${this.resource}/${id}`);
  }

  create(data) {
    return apiClient.post(`/${this.resource}`, data);
  }

  update(id, data) {
    return apiClient.put(`/${this.resource}/${id}`, data);
  }

  delete(id) {
    return apiClient.delete(`/${this.resource}/${id}`);
  }
}
// src/api/UserRepository.js
import { BaseRepository } from './BaseRepository';
import apiClient from './client';

class UserRepository extends BaseRepository {
  constructor() {
    super('users');
  }

  // Custom resource-specific endpoint
  getProfile() {
    return apiClient.get(`/${this.resource}/me/profile`);
  }
}

export default new UserRepository();

Benefits:

  • Drastically reduces boilerplate for large applications with standard CRUD resources.
  • Promotes object-oriented design and consistency.

4. The Centralized Endpoint Registry Pattern

This pattern separates endpoint URLs into a dedicated configuration map or dictionary, decoupling URI routing definitions from the HTTP execution logic.

// src/api/endpoints.js
export const ENDPOINTS = {
  AUTH: {
    LOGIN: '/auth/login',
    REGISTER: '/auth/register',
    REFRESH: '/auth/refresh',
  },
  USERS: {
    BASE: '/users',
    DETAIL: (id) => `/users/${id}`,
    PERMISSIONS: (id) => `/users/${id}/permissions`,
  },
};
// src/api/auth.js
import apiClient from './client';
import { ENDPOINTS } from './endpoints';

export const login = (credentials) => apiClient.post(ENDPOINTS.AUTH.LOGIN, credentials);

Benefits:

  • Single source of truth for API routes.
  • Quick verification of route structures when backend API contracts change.

Summary of Pattern Selection

  • Base Axios Instance: Mandatory foundation for all architectures.
  • Module / Service Pattern: Ideal for small-to-medium applications or APIs where endpoints vary significantly in design.
  • Repository Pattern: Best suited for large, enterprise applications interacting with strictly uniform REST APIs.
  • Endpoint Registry Pattern: Optimal when API versioning and route mapping change frequently.