ProxyBridge
Reusable Next.js App Router proxy for token-based backend APIs.
The package is useful when the browser should not store or read auth tokens.
Requests go through a Next.js route handler, tokens are stored in httpOnly
cookies, and the proxy forwards authenticated requests to the backend.
Browser -> Next.js /api/[...proxy] -> Backend API
What It Does
- Stores access and refresh tokens in
httpOnlycookies. - Forwards client API requests to your backend.
- Adds the access token to backend requests.
- Refreshes the access token when the backend returns an auth error.
- Retries the failed request after a successful refresh.
- Clears cookies on logout or failed refresh.
- Removes token values from auth responses before returning data to the browser.
Installation
npm install proxy-bridge
For local development:
npm install ./proxy-bridge
Basic Usage
Create a shared ProxyBridge instance:
// src/proxy-bridge.ts
import { createProxyBridge } from 'proxy-bridge';
export const proxyBridge = createProxyBridge({
appUrl: process.env.APP_URL!,
backendBaseUrl: 'https://backend.example.com/v1',
cookies: {
access: {
name: 'access_token',
maxAge: 60 * 60,
},
refresh: {
name: 'refresh_token',
maxAge: 60 * 60 * 24 * 7,
},
},
auth: {
refreshEndpoint: 'auth/refresh',
logoutEndpoint: 'auth/logout',
tokenEndpointPatterns: [/^auth\/login$/, /^auth\/refresh$/],
},
});
Use it in a catch-all API route:
// src/app/api/[...proxy]/route.ts
import { proxyBridge } from '@/proxy-bridge';
export const { GET, POST, PUT, PATCH, DELETE } = proxyBridge.handlers;
Then call your API through the Next.js route:
await fetch('/api/users/me');
await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
});
Default URL mapping:
/api/users/me -> https://backend.example.com/v1/users/me
CSR and SSR Usage
CSR with TanStack Query
import { useQuery } from '@tanstack/react-query';
export function useProfile() {
return useQuery({
queryKey: ['profile'],
queryFn: async () => {
const response = await fetch('/api/users/me', {
credentials: 'include',
});
return response.json();
},
});
}
SSR
import { proxyBridge } from '@/proxy-bridge';
export async function getProfile() {
const response = await proxyBridge.fetch('/users/me', {
cache: 'no-store',
});
return response.json();
}
createProxyBridge Parameters
appUrl
The public URL of your Next.js application. It is used by proxyBridge.fetch
when making SSR requests to your internal proxy route.
appUrl: 'https://app.example.com'
Default: no default, required.
routePrefix
The internal route prefix where your catch-all proxy route is mounted.
routePrefix: '/api'
Default: '/api'
backendBaseUrl
Backend API base URL.
backendBaseUrl: 'https://backend.example.com'
Required by default. Optional if buildBackendUrl is provided.
Default: undefined
cookies
Cookie configuration for access and refresh tokens.
cookies: {
access: {
name: 'access_token',
maxAge: 3600,
},
refresh: {
name: 'refresh_token',
maxAge: 604800,
},
}
Cookie options:
{
name: string;
maxAge?: number;
path?: string;
secure?: boolean;
httpOnly?: boolean;
sameSite?: 'strict' | 'lax' | 'none';
}
Defaults:
{
path: '/',
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
}
auth
Auth endpoint and access-token forwarding configuration.
auth: {
refreshEndpoint: 'auth/refresh',
logoutEndpoint: 'auth/logout',
tokenEndpointPatterns: [/^auth\/login$/],
}
Options:
{
refreshEndpoint: string;
logoutEndpoint: string;
tokenEndpointPatterns: RegExp[];
authHeader?: false | ((context) => HeadersInit);
}
Defaults:
authHeader: ({ accessToken }) => ({
Authorization: `Bearer ${accessToken}`,
})
Custom access token header:
auth: {
refreshEndpoint: 'auth/refresh',
logoutEndpoint: 'auth/logout',
tokenEndpointPatterns: [/^auth\/login$/],
authHeader: ({ accessToken }) => ({
'X-Access-Token': accessToken,
}),
}
Disable access token forwarding:
authHeader: false
refresh
Controls when and how refresh token requests are sent.
refresh: {
statusCodes: [401],
tokenTransport: 'body',
tokenBodyKey: 'refreshToken',
}
Options:
{
statusCodes?: number[];
tokenTransport?: 'body' | 'header' | 'cookie';
tokenBodyKey?: string;
tokenHeaderName?: string;
tokenCookieName?: string;
buildRequest?: (context) => RequestInit;
}
Defaults:
{
statusCodes: [401],
tokenTransport: 'body',
tokenBodyKey: 'refreshToken',
tokenHeaderName: 'X-Refresh-Token',
tokenCookieName: cookies.refresh.name,
}
Refresh on multiple status codes:
refresh: {
statusCodes: [401, 419],
}
Send refresh token in a header:
refresh: {
tokenTransport: 'header',
tokenHeaderName: 'X-Refresh-Token',
}
Send refresh token as a cookie header:
refresh: {
tokenTransport: 'cookie',
tokenCookieName: 'refresh_token',
}
Use a fully custom refresh request:
refresh: {
buildRequest: ({ refreshToken }) => ({
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: refreshToken }),
cache: 'no-store',
}),
}
defaultHeaders
Headers added only when the incoming request does not already contain them.
defaultHeaders: {
'Accept-Language': 'az',
}
Default: {}
overrideHeaders
Headers that always override incoming request headers.
overrideHeaders: {
'Accept-Language': 'az',
}
Default: {}
stripRequestHeaders
Request headers that should not be forwarded to the backend.
stripRequestHeaders: ['cookie', 'host']
Default: hop-by-hop headers plus browser cookies.
stripResponseHeaders
Response headers that should not be returned to the browser.
stripResponseHeaders: ['set-cookie']
Default: hop-by-hop headers plus set-cookie.
extractTokens
Custom token extractor for backend responses.
Default supported response shapes:
{
accessToken: string;
refreshToken: string;
}
or:
{
data: {
accessToken: string;
refreshToken: string;
}
}
Custom example:
extractTokens: (payload) => {
const response = payload as {
tokens?: {
access?: string;
refresh?: string;
};
};
return {
accessToken: response.tokens?.access,
refreshToken: response.tokens?.refresh,
};
}
Default: built-in extractor.
sanitizeTokenResponse
Controls whether token values are removed from JSON responses.
sanitizeTokenResponse: 'auth-endpoints'
Options:
false | true | 'auth-endpoints' | 'all-json' | ((context) => unknown)
Defaults:
'auth-endpoints'
Meaning:
auth-endpoints: sanitize only endpoints matchingtokenEndpointPatternsall-json: sanitize all JSON responsestrue: same asall-jsonfalse: do not sanitize- function: use custom sanitizer
buildBackendUrl
Custom backend URL builder.
Use this when the default URL format is not enough.
buildBackendUrl: ({ request, backendPath }) => {
const url = new URL(`https://gateway.example.com/internal/${backendPath}`);
request.nextUrl.searchParams.forEach((value, key) => {
url.searchParams.append(key, value);
});
return url;
}
Default:
`${backendBaseUrl}/${backendPath}`
buildRefreshRequest
Top-level custom refresh request builder.
buildRefreshRequest: ({ refreshToken }) => ({
method: 'POST',
body: JSON.stringify({ refreshToken }),
cache: 'no-store',
})
Prefer refresh.buildRequest for new code.
Default: undefined
Notes
- The main public API is
createProxyBridge. - The package is designed for Next.js App Router route handlers.
- Tokens are stored in
httpOnlycookies by default. - Access token forwarding defaults to
Authorization: Bearer <token>. - Refresh requests default to
401responses. - Concurrent refresh calls are locked, so multiple failed requests share one refresh request.