A robust, production-ready API client built with Axios for Next.js applications with GraphQL support, automatic token refresh, and comprehensive error handling.
- Features
- Architecture
- Installation
- Configuration
- Usage
- Components
- Error Handling
- Token Management
- GraphQL Integration
- Best Practices
- Troubleshooting
- π Automatic Token Refresh - Seamlessly refreshes expired tokens without user intervention
- π Request Queue Management - Queues requests during token refresh to prevent race conditions
- π‘οΈ GraphQL Error Handling - Properly handles GraphQL-specific error structures
- π Server-Side Rendering Support - Compatible with Next.js server components
- π Secure Token Storage - Built-in token management system
- β‘ Retry Logic - Smart retry mechanism with failure prevention
- π Type-Safe - Full TypeScript support with comprehensive type definitions
- πͺ Cookie Management - Automatic credential handling with
withCredentials
api/
βββ api.ts # Main Axios instance with interceptors
βββ example.tsx # Usage example with Next.js page
βββ TokenManager.ts # Token management utility (referenced)
βββ appError.ts # Custom error class (referenced)
βββ README.md # This file
Request β Interceptor (Add Token) β API Call
β
Success?
β
ββββββββ΄βββββββ
β β
YES NO
β β
Return Data 401 Error?
β
βββββββ΄ββββββ
β β
YES NO
β β
Refresh Token Return Error
β
βββββββββ΄βββββββββ
β β
Success Failure
β β
Retry Request Redirect to Login
npm install axios
# or
yarn add axios
# or
pnpm add axiosCreate a .env.local file in your project root:
NEXT_PUBLIC_API_ENDPOINT=https://your-api-endpoint.comThe API client is pre-configured with the following defaults:
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_ENDPOINT,
withCredentials: true, // Enables cookie-based authentication
headers: {
"Content-Type": "application/json",
},
});To modify the configuration, update the api.ts file:
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_ENDPOINT,
timeout: 10000, // Add request timeout
withCredentials: true,
headers: {
"Content-Type": "application/json",
// Add custom headers here
},
});import api from "@/api/api";
// Simple GET request
const response = await api.get("/endpoint");
// POST request with data
const response = await api.post("/endpoint", {
key: "value",
});import api from "@/api/api";
const QUERY = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
const response = await api.post("/graphql", {
query: QUERY,
variables: { id: "123" },
});
const user = response.data.data.user;const MUTATION = `
mutation CreateProduct($input: ProductInput!) {
createProduct(input: $input) {
id
name
price
}
}
`;
const response = await api.post("/graphql", {
query: MUTATION,
variables: {
input: {
name: "New Product",
price: 99.99,
},
},
});import { headers } from "next/headers";
import api from "@/api/api";
const Page = async () => {
const headersList = await headers();
const response = await api.post("/graphql", {
query: YOUR_QUERY,
variables: {
/* your variables */
},
headers: Object.fromEntries(headersList.entries()),
});
return <YourComponent data={response.data.data} />;
};Automatically attaches the access token to every request:
api.interceptors.request.use((config) => {
const token = tokenManager.getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});Handles responses and errors, including:
- GraphQL error transformation
- 401 (Unauthorized) error handling
- Token refresh mechanism
- Request queuing during refresh
Key features:
- Single Refresh - Only one refresh request at a time
- Request Queue - Queues pending requests during refresh
- Failure Prevention - Stops retry attempts after refresh failure
- Automatic Retry - Retries original request with new token
The module uses a custom AppError class for consistent error handling:
throw new AppError(message, statusCode);GraphQL errors are automatically transformed into proper HTTP errors:
// GraphQL error structure
{
errors: [
{
message: "Error message",
extensions: {
code: "UNAUTHENTICATED",
status: 401,
},
},
];
}
// Transformed to AppError
new AppError("Error message", 401);try {
const response = await api.post("/graphql", { query });
} catch (error) {
if (error instanceof AppError) {
console.error(`Error ${error.statusCode}: ${error.message}`);
}
}The module relies on a TokenManager utility with the following methods:
tokenManager.getAccessToken(); // Retrieves current access token
tokenManager.setAccessToken(token); // Stores new access token
tokenManager.clearAccessToken(); // Removes access token
tokenManager.setLogOut(true); // Sets logout state- Request fails with 401 error
- Check if refresh is already in progress
- If not, start refresh process:
- Send refresh mutation to GraphQL endpoint
- Store new access token
- Retry all queued requests
- If refresh fails:
- Clear tokens
- Set logout flag
- Redirect to login page
The module uses this GraphQL mutation to refresh tokens:
mutation {
refreshAccessToken {
access_token
}
}| GraphQL Code | HTTP Status | Action |
|---|---|---|
| UNAUTHENTICATED | 401 | Trigger token refresh |
| Other errors | 500 or custom | Return error to caller |
Keep the API client server-side only:
import "server-only";Wrap components using the API with error boundaries:
import ErrorHandler from "@/components/Error/errorHandler";
try {
const data = await fetchData();
return <Component data={data} />;
} catch (error) {
return <ErrorHandler message={error.message} />;
}Always define TypeScript interfaces for your data:
interface Product {
id: string;
name: string;
price: number;
}
const response = await api.post<{ data: { products: Product[] } }>("/graphql", {
query: PRODUCTS_QUERY,
});- Use field selection in GraphQL queries
- Implement pagination
- Cache responses when appropriate
const OPTIMIZED_QUERY = `
query GetProducts($limit: Int, $page: Int) {
products(limit: $limit, page: $page) {
docs {
id
name
price
}
meta {
total
totalPages
}
}
}
`;Cause: Refresh token endpoint also returns 401
Solution: Ensure the refresh endpoint uses a different authentication method (refresh token cookie)
Cause: Queued requests not being resolved properly
Solution: Check that refreshSubscribers array is properly managed
Cause: withCredentials: true requires proper CORS configuration on the server
Solution: Server must set:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: <specific-origin>
Cause: GraphQL response structure mismatch
Solution: Always check for errors before accessing data:
if (response.data.errors) {
// Handle error
}
const result = response.data.data;-
Enable Axios Logging:
api.interceptors.request.use((config) => { console.log("Request:", config); return config; });
-
Check Token Storage:
console.log("Token:", tokenManager.getAccessToken());
-
Monitor Refresh State:
console.log("Is Refreshing:", isRefreshing); console.log("Refresh Failed:", refreshFailed);
See example.tsx for a complete Next.js page implementation showing:
- GraphQL query definition
- Server-side data fetching
- Error handling
- Component rendering with fetched data
When modifying the API configuration:
- Ensure backward compatibility
- Update type definitions
- Test token refresh flow
- Update this documentation
- Add error handling for edge cases
This module is part of the project and follows the same license terms.
Last Updated: November 2025
Version: 1.0.0