API Security with NextJS Rewrites and Proxy
Introduction
NextJS Proxy and Rewrites are a powerful duo that helps manage and route requests flexibly.
- NextJS Rewrites act as an internal proxy, essentially mapping one URL path to another. Its biggest advantage is hiding the actual URL of the underlying core server, while transforming requests from Cross-Origin to Same-Origin on the client interface.
- NextJS Proxy allows you to execute code before a request is completed. As a result, you can easily intervene to implement features like Authentication, authorization, or centralized Rate Limiting, reducing the load on the core API server and maximizing performance. Previously, this function was called middleware, the name change to Proxy aims to confirm that this is an ultra-lightweight Gateway layer:
- It should only be used for tasks such as Routing, Rewrite, Redirect and managing Header/Cookie.
- NextJS encourages moving Granular Authorization logic or complex session management into Server Components or Server Actions to take advantage of the stable NodeJS environment and avoid slowing down the response speed of the Proxy.
This is the execution order of how a request passes through these layers, these configurations follow the Server-side routing mechanism, so they apply to both page navigation and API requests.
- headers: used to set Response Headers (Headers returned from the NextJS Server to the browser). When the browser accesses a URL that matches the source, NextJS will send these headers along, it does not automatically attach headers to requests called from the Client (such as fetch or axios) to the Server, or for internal requests when NextJS renders the page.
- Main purpose: Security configuration (CORS, Content-Security-Policy, X-Frame-Options) or Cache-Control.
- Example: You want every web page returned to the user to have the X-Custom-Header.
- redirects: applied to both Page and API, as long as the path matches the source configuration you set.
- For Page (User Interface) This is the most common case. When a user accesses an old url, NextJS will respond to that request with a response header field location having the value of the new url, then the browser will automatically redirect to that new url.
- For API (Route Handlers) redirects also work with API requests (fetch, axios). Behavior: When calling fetch('/api/old-data'), NextJS returns a redirect response. Most HTTP libraries (such as fetch or axios) will automatically follow that redirect path to get data from the new URL. Note: Unlike rewrites (running silently), redirects will make the Client perform a second request to the new URL.
- proxy: can handle intermediate code for security issues such as JWT, Cookie, Rate limit before sending the actual request to the core service. It can return an error, redirect or allow it to continue to the layers below.
- beforeFiles: If matched here, the request will be redirected immediately (and return to Proxy with the new URL). Often used for system-wide maintenance pages.
- Filesystem Check: NextJS checks if you have any physical files that match (such as page.tsx or route.ts in the app router).
- afterFiles: If it does not find a file, this configuration is checked. Often used as an API Proxy, only calling the Backend when the Frontend does not have that route.
- Dynamic Routes: Checks dynamic routes like [id].
- fallback: If all the above steps do not match, this configuration runs. Used when you are in the process of deploying a new system and want to redirect pages under development back to the Legacy System.
Detail
First, please update the next.config.ts file.
import type {NextConfig} from 'next'
const nextConfig: NextConfig = {
async headers() {
return [
{
source: '/header/:path*',
headers: [
{key: 'X-Custom-Header', value: 'custom header value'},
],
},
]
},
async redirects() {
return [
{
source: '/api/old',
destination: '/api/new',
permanent: false,
},
{
source: '/old-page',
destination: '/new-page',
permanent: true,
},
]
},
async rewrites() {
return {
beforeFiles: [
{
source: '/before-files/:path*',
destination: '/api/before-files/:path*',
},
{
source: '/before-files/has-header/:path*',
has: [{type: 'header', key: 'x-header-field', value: 'secret-value'}],
destination: '/maintenance',
},
],
afterFiles: [
{
source: '/after-files/:path*',
destination: '/api/after-files/:path*',
},
],
fallback: [
{
source: '/:path*',
destination: '/legacy-system/:path*',
},
],
}
},
}
export default nextConfig
headers: add fields to the response header for responses from the NextJS server.
redirects: if matched with the source, it will redirect directly from here instead of checking subsequent layers.
- permanent: false, status code 307 Temporary Redirect.
- permanent: true, status code 308 Permanent Redirect.
beforeFiles: runs before checking files in the app router.
afterFiles: runs only if there is no page.tsx or route.ts file.
fallback: the final layer when no source matches.
Note that the default config in afterFiles, if used independently, can be written simply like this:
import type {NextConfig} from 'next'
const nextConfig: NextConfig = {
async rewrites() {
return [
{
source: '/after-files/:path*',
destination: '/api/after-files/:path*',
},
]
},
}
export default nextConfig
The proxy.ts file must be unique and located at the same level as the app folder.
import {NextResponse, type NextRequest} from 'next/server'
export function proxy(request: NextRequest) {
const {pathname} = request.nextUrl
if (pathname.includes('error')) {
return NextResponse.json({error: 'Forbidden'}, {status: 403})
}
if (pathname.includes('redirect')) {
return NextResponse.redirect(new URL('/new-url', request.url), 307)
}
if (pathname.includes('next')) {
const headers = new Headers(request.headers)
headers.set('x-header-field', 'secret-value')
return NextResponse.next({request: {headers}})
}
return NextResponse.next()
}
export const config = {
matcher: ['/api/:path*'],
}
In proxy, logic can be applied to handle complex cases such as:
- NextResponse.json: responds to the client.
- NextResponse.redirect: redirects to another page.
- NextResponse.next: you can pass additional headers after customizing, only when calling this function does it continue down to rewrites beforeFiles.
- Please note that only the APIs matching this configuration will be processed within the proxy.
The app/headers/hook.ts file is just a hook to call the API.
import {useMutation} from '@tanstack/react-query'
import {message} from 'antd'
export const useCallApi = () => {
return useMutation({
mutationFn: async (endpoint: string) => {
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({}))
throw new Error(
errorData.message ||
`Error ${response.status}: ${response.statusText}`
)
}
return response.json()
},
onSuccess: data => {
message.success('Request successful')
},
onError: (error: Error) => {
message.error(error.message)
},
})
}
The app/headers/page.tsx file is used for you to test api call and page navigation cases. Note that because there are many examples, I will not create all full pages, please create your own pages to check according to your needs.
'use client'
import {
ArrowRightOutlined,
BugOutlined,
FastForwardOutlined,
FileSearchOutlined,
GlobalOutlined,
SendOutlined,
StepForwardOutlined,
ToolOutlined,
} from '@ant-design/icons'
import {Button, Card, Typography} from 'antd'
import {useRouter} from 'next/navigation'
import {useCallApi} from './hook'
const {Text} = Typography
const apiButtons = [
{
label: 'Call API',
endpoint: '/api/old',
icon: <SendOutlined />,
color: 'bg-blue-600',
},
{
label: 'Call Error',
endpoint: '/api/error',
icon: <BugOutlined />,
color: 'bg-red-600',
},
{
label: 'Call Redirect',
endpoint: '/api/redirect',
icon: <ArrowRightOutlined />,
color: 'bg-orange-500',
},
{
label: 'Call Next',
endpoint: '/api/next',
icon: <FastForwardOutlined />,
color: 'bg-green-600',
},
]
const navButtons = [
{
label: 'Go Before Files',
path: '/before-files/test',
icon: <GlobalOutlined />,
},
{
label: 'Go Before (Has Header)',
path: '/api/next/before-files-has',
icon: <ToolOutlined />,
info: 'Requires x-maintenance header',
},
{
label: 'Go After Files',
path: '/after-file/test',
icon: <FileSearchOutlined />,
},
{
label: 'Go Fallback',
path: '/any-random-path',
icon: <StepForwardOutlined />,
},
]
export default function HeadersPage() {
const router = useRouter()
const {mutate, isPending, data, error, variables} = useCallApi()
return (
<div className="flex flex-col items-center justify-center min-h-[600px] p-8 bg-slate-50">
<Card
title="Testing Panel"
className="w-full max-w-md shadow-lg rounded-2xl"
>
<div className="flex flex-col gap-3">
<Text
strong
className="text-gray-500 text-[11px] uppercase tracking-wider"
>
API Requests
</Text>
{apiButtons.map(btn => (
<Button
key={btn.endpoint}
type="primary"
icon={btn.icon}
loading={isPending && variables === btn.endpoint}
disabled={isPending}
onClick={() => mutate(btn.endpoint)}
className={`h-10 ${btn.color} hover:opacity-90 rounded-lg font-medium shadow-sm transition-all flex items-center justify-center`}
block
>
{btn.label}
</Button>
))}
<Text
strong
className="text-gray-500 text-[11px] uppercase tracking-wider"
>
Page Routing
</Text>
{navButtons.map(nav => (
<Button
key={nav.path}
type="default"
icon={nav.icon}
onClick={() => router.push(nav.path)}
className="h-10 border-gray-300 hover:border-blue-500 hover:text-blue-500 rounded-lg font-medium flex items-center justify-center text-gray-600"
block
>
{nav.label}
</Button>
))}
{(data || error) && <hr className="my-4 border-gray-100" />}
{data && (
<div className="p-4 bg-green-50 border border-green-200 rounded-xl">
<Text type="success" strong className="block mb-1 text-xs">
Success ({variables}):
</Text>
<pre className="text-[10px] text-green-800 overflow-auto max-h-40 font-mono">
{JSON.stringify(data, null, 2)}
</pre>
</div>
)}
{error && (
<div className="p-4 bg-red-50 border border-red-200 rounded-xl">
<Text type="danger" strong className="text-xs">
Failed ({variables}):
</Text>
<p className="text-[11px] text-red-600 mt-1 font-mono">
{error.message}
</p>
</div>
)}
</div>
</Card>
</div>
)
}
Check the results as follows
This is custom header
Happy coding!
Comments
Post a Comment