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.