Use Next.js as a real server — not just a React wrapper.
next-api-bridge is a cookie-aware API gateway for Next.js App Router. It sits between your UI and your external backend, handling cookies, headers, auth, and session flow entirely on the server.
Most Next.js apps with an external backend fall into this pattern:
Client → useEffect → fetch → Backend API
This means tokens in localStorage, broken HttpOnly cookies, duplicated API logic, and a client component that knows too much about your auth.
Client (UI only)
↓
Server Action (Next.js)
↓
next-api-bridge
↓
External Backend (NestJS · Laravel · Django · Express…)
Your client triggers actions. Your server owns the session. Your backend never sees the browser directly.
npm install next-api-bridgeOptional — for toast helpers:
npm install sonnerRequires: Next.js 13+ (App Router), Node.js server environment. Server Actions, Route Handlers, or Server Components only — not for browser fetch.
// src/server/api.ts
import { createNextApiBridge } from 'next-api-bridge';
export const api = createNextApiBridge({
baseUrl: process.env.API_URL!,
});// server/auth/action.ts
'use server';
import { redirect } from 'next/navigation';
import { api } from '@/server/api';
import { getCleanFormData, validateRedirectPath } from 'next-api-bridge/form';
export async function signIn(_prev: unknown, data: FormData) {
const body = getCleanFormData(data, { delete: ['redirectPath'] });
const response = await api.post('/auth/login', body);
if (response.success) redirect(validateRedirectPath(data.get('redirectPath') as string));
return { formdata: body, ...response };
}// components/login-form.tsx
'use client';
import { useActionState } from 'react';
import { signIn } from '@/server/auth/action';
export default function LoginForm() {
const [state, action, isPending] = useActionState(signIn, null);
return (
<form action={action}>
<input name="email" type="email" />
<input name="password" type="password" />
<button disabled={isPending}>Sign in</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}No useEffect. No useState for auth. No token management on the client.
createNextApiBridge({
baseUrl: string; // Required. Your backend URL.
cookiePrefix?: string; // Default: 'nab_'. Namespaces backend cookies in Next.js.
apiKey?: string; // Optional API key.
apiKeyHeader?: string; // Header name for the API key.
auth?: BearerAuthConfig; // Bearer token from a cookie.
verbose?: string; // 'request,body,response' for debug logging.
});Reads a token from a cookie and adds it as an Authorization header automatically:
export const api = createNextApiBridge({
baseUrl: process.env.API_URL!,
auth: {
type: 'bearer',
tokenCookie: 'accessToken', // reads nab_accessToken cookie
header: 'Authorization',
prefix: 'Bearer',
},
});export const api = createNextApiBridge({
baseUrl: process.env.API_URL!,
apiKey: process.env.API_KEY,
apiKeyHeader: 'X-API-Key',
});api.get('/users/me');
api.post('/auth/login', body);
api.patch('/users/me', body);
api.put('/settings', body);
api.delete('/sessions/current');All methods return:
{
success: boolean;
message: string;
body: T | null;
headers?: Headers;
}// Query params
api.get('/events', { query: { page: 1, search: 'conf' } });
// Path params
api.get('/events', { params: ['event-id'] });
// Cache control
api.get('/static-data', { cache: 'force-cache' });
// File upload
api.post('/upload', formData, { isMultipart: true });When your backend responds with Set-Cookie: accessToken=abc123, next-api-bridge captures it and stores it in Next.js as nab_accessToken. On the next request, it strips the prefix and forwards Cookie: accessToken=abc123 to your backend transparently.
HttpOnly session cookies work out of the box — no workarounds needed.
await api.setCookie('sessionid', 'abc123', { httpOnly: true, maxAge: 3600 });
await api.getCookie('accessToken');
await api.deleteCookies(['accessToken', 'refreshToken']); // or pass nothing to delete allgetCleanFormData replaces Object.fromEntries() with something smarter — it strips empty fields, Next.js internals, and can coerce types:
import { getCleanFormData } from 'next-api-bridge/form';
const body = getCleanFormData(data, {
delete: ['redirectPath'],
jsonParse: ['deviceInfo'],
boolean: ['isActive'],
number: ['price', 'quantity'],
date: ['startsAt'],
});import {
validateRedirectPath, // Returns '/' if path is invalid or external
buildUrlWithParams, // Builds '/path?key=value' strings
reloadPage, // Revalidates a page after mutation
} from 'next-api-bridge/form';// In your root layout
import { Toaster } from 'sonner';
<Toaster />
// In a client component
import { showResponseToast, showResponseToastAndReload } from 'next-api-bridge/form';
showResponseToast({ state });
showResponseToastAndReload({ state, path: '/dashboard' });Server Components can fetch directly — no loading state, no useEffect:
// app/layout.tsx
export default async function RootLayout({ children }) {
const user = await getUser();
const { body } = await api.get('/memberships');
return (
<html><body>
<AppProvider user={user} memberships={body?.members ?? []}>
{children}
</AppProvider>
</body></html>
);
}| Route | Approach |
|---|---|
| Auth, forms, mutations | Use next-api-bridge via Server Actions |
| Protected data reads | Use next-api-bridge in Server Components |
| Fully public / static data | Direct fetch() is fine |
// Core
import { createNextApiBridge, NextApiBridgeClient } from 'next-api-bridge';
import type { ApiBridgeOptions, ApiBridgeResponse, BearerAuthConfig } from 'next-api-bridge';
// Helpers
import {
getCleanFormData,
reloadPage,
validateRedirectPath,
buildUrlWithParams,
showResponseToast,
showResponseToastAndReload,
} from 'next-api-bridge/form';MIT