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:


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:


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:


Summary of Pattern Selection