Generated code
Route handlers (Next.js)
One file per API path, acting as a transparent proxy from your Next.js app to the backend. Each handler:
- forwards the method, query string (repeated keys included), path params (URL-encoded) and the raw body — JSON,
multipart/form-datauploads, binary; empty bodies are fine - forwards the
Authorization,Content-Type,AcceptandAccept-Languageheaders, and withcookieNameturns the JWT cookie intoAuthorization: Bearer <token>when the client didn't send one - returns the backend response as-is: status, headers, every
Set-Cookie, and the body — JSON, files, text (204/205/304without a body) - keeps backend error responses intact (e.g. a
422with validation details), and returns500 { success: false, message }only if the backend can't be reached
The generated files are small — the proxy logic lives in fetchBackend.ts:
// src/app/api/users/[id]/route.ts
import { fetchBackend, forwardHeaders, readBody, toResponse } from '../../../../lib/fetchBackend';
async function proxy(request: Request, context: any) {
try {
const API_URL = process.env.API_URL || '';
const params = await context.params;
const { search } = new URL(request.url);
const response = await fetchBackend(`${API_URL}/users/${encodeURIComponent(params.id)}${search}`, {
method: request.method,
headers: await forwardHeaders(request),
body: request.method === 'GET' ? undefined : await readBody(request),
});
return toResponse(response);
} catch (error) {
console.error(`[${request.method} /users/{id}]`, error);
return Response.json({ success: false, message: 'Internal Server Error' }, { status: 500 });
}
}
export const GET = proxy;
export const PATCH = proxy;
export const DELETE = proxy;App Router (framework: 'nextjs') — one route.ts per path:
src/app/api/
users/
route.ts ← GET /users, POST /users
[id]/
route.ts ← GET, PATCH, DELETE /users/{id}Pages Router (framework: 'nextjs-pages') — the last segment becomes the file name, and all methods live in one handler:
pages/api/
users.ts ← GET /users, POST /users
users/
[id].ts ← GET, PATCH, DELETE /users/{id}Every API route exports config = { api: { bodyParser: false } } so the body reaches the backend untouched, and answers 405 with an Allow header for methods the spec doesn't define.
Path parameter names
Next.js requires the same slug name at the same folder level. If your spec has /users/{id} and /users/{userId}/posts, both use the first name found ([id]), and services/hooks use that same name.
Typed services
One folder per OpenAPI tag (the first tag of each operation):
src/services/
index.ts ← re-exports every service: import { usersService } from '@/services'
users/
index.ts ← usersService with one async method per operation
types.ts ← schemas + <Method>Response / <Method>Body / <Method>Params// src/services/users/index.ts (excerpt)
const usersService = {
async list(params: ListParams = {} as ListParams): Promise<ListResponse> {
const { data } = await apiClient.get('/api/users', { params });
return data;
},
async create(body: CreateBody): Promise<CreateResponse> {
const { data } = await apiClient.post('/api/users', body);
return data;
},
};Method names come from the operationId, with NestJS-style controller prefixes removed (UsersController_list → list). If two operations in the same tag would get the same name, both use the qualified name instead (usersList, adminList). Operations without an operationId get one from the method and path (GET /health → getHealth).
Types support objects, arrays, enums, $ref (including components/parameters, requestBodies and responses), allOf / oneOf / anyOf, and nullability (nullable: true and type: ['string', 'null']). Schema names that aren't valid identifiers are sanitized (Page«User» → Page_User_). An operation type never shadows a schema: if your spec has a LoginResponse schema, the login response type becomes LoginResponseData.
File uploads: operations whose body is only multipart/form-data are sent with postForm / putForm / patchForm. Pass a plain object — format: binary fields are typed as Blob (a File works) and arrays repeat the key (photos, not photos[], as multer and most upload middlewares expect) — or a FormData you built yourself, which is sent as-is.
await productsService.create({ name: 'Pizza', photos: [file1, file2] });In Next.js projects services call your generated routes (e.g. /api/users). In React projects they call the backend directly: <baseUrl><path>, with the base URL read from an env variable (import.meta.env.VITE_API_URL by default).
React hooks
Only for framework: 'react'. One file per tag in hooksOut, importing the matching service with a relative path.
hooksMode: 'react-query' (default)
const tag = "users";
export function useList(params?: ListParams) {
return useQuery<ListResponse>({
queryKey: [tag, 'list', params],
queryFn: () => usersService.list(params),
});
}
export function useGet(id: string) {
return useQuery<GetResponse>({
queryKey: [tag, 'get', id],
queryFn: () => usersService.get(id),
enabled: !!id,
});
}
export function useCreate() {
const queryClient = useQueryClient();
return useMutation<CreateResponse, Error, { body: CreateBody }>({
mutationFn: (vars) => usersService.create(vars.body),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["users"] });
},
});
}GET→useQuery, keyed by[tag, operation, ...pathParams, params]- other methods →
useMutation; on success every query of the tag is invalidated - queries with path params only run when all params are truthy
hooksMode: 'fetch'
No extra dependency — useState + useEffect:
GEThooks return{ data, loading, error }and refetch when path params orparamschange- mutation hooks return
{ mutate, loading, error }, wheremutate(vars)returns the response
apiClient.ts
A browser Axios instance (default src/lib/apiClient.ts) used by the services:
- with
cookieName, reads the JWT withjs-cookieand sendsAuthorization: Bearer <token> - on
401of a request that carried the token, removes the cookie and redirects tounauthorizedRedirect(default/auth), unless the page is already there. A401without a token (e.g. a wrong password) is left to the caller - with
deviceTracking: true, addsx-device-id,x-device-user-agent,x-device-browser,x-device-osandx-device-typeheaders
WARNING
js-cookie can only read cookies that are not httpOnly. In Next.js the route handlers already forward the cookie server-side, so you can keep the cookie httpOnly and leave the browser without the token.
fetchBackend.ts
A server-only helper (default src/lib/fetchBackend.ts) with the proxy logic the route handlers use:
fetchBackend(url, { method, headers, body })— calls the backend and returns a fetch-like response that keeps the raw bytes (json(),text(),arrayBuffer(),headers.getSetCookie())forwardHeaders(request)— the client headers passed on to the backend (authorization,content-type,accept,accept-language,user-agent,x-forwarded-for,x-real-ip), plus the JWT cookie as a Bearer token whencookieNameis set (vianext/headersin the App Router,req.cookiesin the Pages Router)x-forwarded-forkeeps the real client IP (Next.js fills it when no reverse proxy did), so per-IP rate limits and logs on the backend don't see every user as the Next.js server. Configure the backend to trust only the proxy hops in front of it (Expresstrust proxy), and put Next.js behind a reverse proxy that sets the header — otherwise a client could send its own value.
readBody(request)— the incoming body, untouchedtoResponse(response)(App Router) /sendResponse(res, response)(Pages Router) — sends the backend response back with its status, headers andSet-Cookie- timeout via
fetchBackend.timeout(default 15s) - set
BACKEND_IGNORE_SSL=trueto accept self-signed certificates in development
You can also call fetchBackend from Server Components or your own route handlers.