@codefied-codepix/api
v1.0.1
Published
Shared HTTP transport layer for Next.js apps — Axios setup, interceptors, error normalization, token refresh, and retry logic.
Readme
@codefied-codepix/api
Shared HTTP transport layer for Next.js and Node.js applications. Handles Axios setup, interceptors, error normalization, token refresh, and retry logic.
This package is transport only — it does not define business endpoints. Each application owns its own API modules, routes, and domain types.
Requirements
- Node.js 18+
- Works with Next.js (App Router, Pages Router), React, or any TypeScript/JavaScript project
Installation
Published on the npm registry.
npm install @codefied-codepix/api@latestpnpm add @codefied-codepix/api@latestyarn add @codefied-codepix/api@latestOr add to package.json:
{
"dependencies": {
"@codefied-codepix/api": "^1.0.0"
}
}Monorepo (workspace)
If you vendor this package inside a monorepo:
{
"dependencies": {
"@codefied-codepix/api": "workspace:*"
}
}Quick start
1. Create a client
Create a single API client per runtime context (browser singleton, or per-request on the server). The package does not read environment variables — you inject configuration.
// lib/api/client.ts
import { initializeApi } from "@codefied-codepix/api";
export const api = initializeApi({
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "",
authMode: "none",
retry: { retries: 1 },
});2. Define feature modules in your app
Keep endpoint paths and response types in your application, not in this package.
// lib/api/features/users.ts
import { api } from "../client";
export type User = {
id: string;
name: string;
email: string;
};
export async function getUser(id: string): Promise<User> {
return api.get<User>(`/users/${id}`);
}
export async function updateUser(id: string, data: Partial<User>): Promise<User> {
return api.patch<User>(`/users/${id}`, data);
}3. Use anywhere in your app
import { getUser } from "@/lib/api/features/users";
const user = await getUser("123");Recommended project structure
lib/api/
├── client.ts # initializeApi() — browser singleton
├── server.ts # optional: per-request client for RSC / Server Actions
├── types.ts # app-owned DTOs
└── features/
├── users.ts # domain endpoints
└── products.tsConfiguration
initializeApi({
baseURL: string; // required
authMode?: "bearer" | "cookie" | "none"; // default: "bearer"
getAccessToken?: () => string | null | Promise<string | null>;
setAccessToken?: (token: string) => void | Promise<void>;
refreshToken?: () => Promise<string | void>;
onAuthFailure?: () => void | Promise<void>;
timeout?: number; // default: 30000
withCredentials?: boolean; // auto true when authMode is "cookie"
retry?: RetryOptions | false; // default: 2 retries on network / 408 / 429 / 5xx
headers?: Record<string, string>;
unwrapEnvelope?: boolean; // default: true
});Auth modes
| Mode | Behavior | You provide |
| -------- | ----------------------------------------- | ------------------------------------------- |
| bearer | Sets Authorization: Bearer <token> | getAccessToken, optional setAccessToken |
| cookie | Sends cookies via withCredentials: true | refreshToken for cookie-based refresh |
| none | No auth headers | — |
Usage examples
Public API (no authentication)
import { initializeApi } from "@codefied-codepix/api";
export const api = initializeApi({
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "",
authMode: "none",
retry: { retries: 1 },
});Bearer token with refresh
import { initializeApi } from "@codefied-codepix/api";
let accessToken: string | null = null;
export const api = initializeApi({
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "",
authMode: "bearer",
getAccessToken: () => accessToken,
setAccessToken: (token) => {
accessToken = token;
},
refreshToken: async () => {
const response = await fetch("/api/auth/refresh", {
method: "POST",
credentials: "include",
});
if (!response.ok) {
throw new Error("Failed to refresh token");
}
const { accessToken: token } = await response.json();
return token;
},
onAuthFailure: () => {
window.location.href = "/login";
},
});Cookie-based authentication
When refreshToken must call the same client instance, use a ref to avoid a circular dependency:
import { initializeApi, type ApiClient } from "@codefied-codepix/api";
const apiRef: { current: ApiClient | null } = { current: null };
const api = initializeApi({
baseURL: process.env.NEXT_PUBLIC_API_URL ?? "",
authMode: "cookie",
withCredentials: true,
refreshToken: async () => {
await apiRef.current!.post("/auth/refresh", undefined, {
_skipAuthRefresh: true,
});
},
onAuthFailure: () => {
window.location.href = "/login";
},
});
apiRef.current = api;
export { api };Next.js Server Components / Server Actions
This package does not import next/headers. Create a per-request client in your app:
import { cookies } from "next/headers";
import { initializeApi } from "@codefied-codepix/api";
export function createServerApi() {
const cookieStore = cookies();
return initializeApi({
baseURL: process.env.API_URL!,
authMode: "cookie",
headers: { Cookie: cookieStore.toString() },
refreshToken: async () => {
// your server-side refresh logic
},
});
}ApiClient methods
const api = initializeApi({ baseURL: "https://api.example.com" });
await api.get<T>(url, config?);
await api.post<T>(url, body?, config?);
await api.put<T>(url, body?, config?);
await api.patch<T>(url, body?, config?);
await api.delete<T>(url, config?);
await api.request<T>(config);
api.getRawAdapter(); // escape hatch for testing or custom transportPer-request options
await api.get("/reports/export", {
unwrapEnvelope: false,
_skipAuthRefresh: true,
params: { page: 1 },
signal: abortController.signal,
});Backend response contract
The client expects these shapes from your API when unwrapEnvelope is enabled (default).
Success:
{
"success": true,
"data": {}
}Error:
{
"statusCode": 400,
"message": "Validation failed",
"code": "VALIDATION_ERROR"
}Errors are normalized into ApiError with statusCode, message, code, and helpers (isAuthError, isNetworkError, isRetryable).
Error handling
import { ApiError } from "@codefied-codepix/api";
// or: import { ApiError } from "@codefied-codepix/api/errors";
try {
await api.get("/resource/123");
} catch (error) {
if (error instanceof ApiError) {
if (error.isAuthError) {
// handle unauthorized
}
if (error.code === "VALIDATION_ERROR") {
// handle domain error
}
console.error(error.statusCode, error.message);
}
}Token refresh
On a 401 response:
- If
refreshTokenis configured and the request has not been retried, one refresh runs. - Concurrent
401s share the same in-flight refresh (single-flight). - On success, the original request is retried once.
- On failure,
onAuthFailureruns and the error is thrown.
Use _skipAuthRefresh: true on the refresh endpoint to prevent infinite loops.
Retry behavior
By default, requests retry up to 2 times with exponential backoff on:
- Network errors and timeouts
- HTTP
408,429, and5xx
Retries are skipped for auth errors and most 4xx responses. Disable with retry: false.
initializeApi({
baseURL: "https://api.example.com",
retry: {
retries: 3,
baseDelay: 500,
retryOn: [408, 429, 502, 503],
},
});Exports
| Import path | Exports |
| ------------------------------ | --------------------------------------------------------------------- |
| @codefied-codepix/api | initializeApi, ApiClient, ApiError, normalizeError, types |
| @codefied-codepix/api/errors | ApiError |
| @codefied-codepix/api/types | ApiSuccess, ApiErrorBody, RequestConfig, InitializeApiOptions |
What belongs in this package vs. your app
In this package (transport):
- HTTP client and Axios adapter
- Request / response interceptors
- Error normalization and
ApiError - Token refresh orchestration
- Retry policy
In your application (domain):
baseURLand environment variables- Auth token storage and refresh implementation
- Endpoint paths and API routes
- Domain types and DTOs
- Feature modules and React hooks
Do not add to this package:
- Business endpoint definitions
- Domain-specific types
- UI hooks (
useUsers, etc.)
