Backend for Frontend pattern in Next.js
Next.js supports the Backend for Frontend pattern, which lets you create public endpoints to handle HTTP requests and return any content type, not just HTML. You can access data sources and perform side effects like updating remote data. Backend capabilities serve as an API layer that is publicly reachable, handles any HTTP request, and can return any content type.
Creating a Next.js project with API scaffolding
Using create-next-app with the --api flag automatically includes an example route.ts in the new project's app/ folder, demonstrating how to create an API endpoint. Command: pnpm create next-app --api, npx create-next-app@latest --api, yarn create next-app --api, or bun create next-app --api.
Route Handler file convention
Route Handlers are created using the route.ts or route.js file convention. They are public HTTP endpoints that any client can access. The handler exports functions named GET, POST, PUT, DELETE, PATCH, HEAD, or OPTIONS that handle the respective HTTP methods.
Route Handler error handling with try/catch
Use try/catch blocks for Route Handler operations that may throw an exception. Catch errors and return appropriate Response objects with status codes. Avoid exposing sensitive information in error messages sent to the client.
Route Handler content types
Route Handlers can serve non-UI responses including JSON, XML, images, files, and plain text. Next.js uses file conventions for common endpoints like sitemap.xml, opengraph-image.jpg, favicon, manifest.json, and robots.txt. Custom endpoints can be defined such as llms.txt, rss.xml, and .well-known directories.
Custom Route Handler example: RSS feed
For app/rss.xml/route.ts, create a Route Handler that fetches RSS data, formats it as XML, and returns it with 'content-type: application/xml' header. Example: export async function GET(request: Request) with fetch, JSON parsing, XML template construction, and proper headers set.
Content negotiation with rewrites and Accept header
Use rewrites in next.config.js with header matching to serve different content types from the same URL based on the request's Accept header. For example, serve HTML to browsers and raw Markdown to AI agents from the same URLs. Configure rewrite with has array containing type 'header', key 'accept', and regex value matching text/markdown.
Vary header for content negotiation caching
Set the Vary: Accept response header when implementing content negotiation to tell caches that the response body depends on the Accept request header. Without it, a shared cache could serve incorrect content (e.g., cached Markdown to a browser or vice versa). Most hosting providers already include Accept in their cache key, but setting Vary explicitly ensures correct behavior across all CDNs and proxy caches.
Consuming request payloads in Route Handlers
Use Request instance methods like .json(), .formData(), or .text() to access the request body in a Route Handler. GET and HEAD requests do not carry a body. Only read the request body once; clone the request with request.clone() if you need to read it again.
Validate request data before use
Always validate data before passing it to other systems. Check input in Route Handlers before forwarding requests or using the data in operations like sending emails.
Reading request body only once
A request body can only be read once. If you need to read it multiple times, clone the request first using request.clone(). Attempting to read the original request body again after reading it once will throw an error.
Manipulating data in Route Handlers
Route Handlers can transform, filter, and aggregate data from one or more sources. This keeps logic out of the frontend and avoids exposing internal systems. Offload heavy computations to the server to reduce client battery and data usage.
Route Handler as data transformation example
A POST Route Handler at /app/api/weather/route.ts can accept geo-location data, fetch weather from an external API using URLSearchParams, parse the response, and return transformed JSON. Use POST instead of GET to avoid caching geo-location data in URLs.
Proxying requests in Route Handlers
Use a Route Handler as a proxy to another backend. Add validation logic before forwarding the request. Clone the incoming request, validate it, then create a new Request with the proxy URL and original request properties. Catch and handle errors appropriately.
NextRequest and NextResponse extensions
Next.js extends Request and Response Web APIs with methods that simplify common operations. NextRequest includes the nextUrl property exposing parsed values from the incoming request, making it easier to access pathname and search params. NextResponse provides helpers like next(), json(), redirect(), and rewrite(). Both provide methods for reading and manipulating cookies. NextRequest can be passed to functions expecting Request, and NextResponse can be returned where Response is expected.
NextRequest.nextUrl property
NextRequest includes the nextUrl property which exposes parsed values from the incoming request. This property provides easy access to request pathname, search parameters, and other URL components.
NextResponse helper methods
NextResponse provides helper methods: next() for passing to the next middleware/handler, json() for returning JSON responses, redirect() for redirects, and rewrite() for rewrites.
Webhooks and callback URLs in Route Handlers
Use Route Handlers to receive event notifications from third-party applications. Examples include revalidating a route when content changes in a CMS. Configure the CMS to call a specific Route Handler endpoint on changes. For callback URLs, verify the response after a third-party flow completes and decide where to redirect the user.
Webhook revalidation example
A GET Route Handler can check a token from query parameters against process.env.REVALIDATE_SECRET_TOKEN for security. Extract a tag parameter and call revalidateTag(tag, 'max') to revalidate cached routes. Return 401 if token is invalid, 400 if tag is missing, and 200 with success: true when successful.
Callback URL security - prevent open redirects
When handling callback URLs with redirect_url query parameters, verify that the destination origin matches the request origin. Only allow same-origin destinations to prevent open redirect vulnerabilities.
Setting cookies in Route Handler responses
Use response.cookies.set() to set cookies in Route Handler responses. Parameters include: value (the cookie value), name (cookie name), path (cookie path, e.g., '/'), secure (true for HTTPS only), httpOnly (true to prevent client-side access), and expires (undefined for session cookies).
Route Handler redirects
Route Handlers can perform redirects using the redirect() function from 'next/navigation'. This can be used to redirect requests to external URLs.
Proxy file configuration
Only one proxy file is allowed per project. Use config.matcher to target specific paths. The proxy file (proxy.ts or proxy.js) exports a proxy function and config object to generate a response before the request reaches a route path.
Proxy function authentication example
A proxy function can check authentication status and return an error response before allowing the request to proceed. For example, check isAuthenticated(request) and return Response.json({ success: false, message: 'authentication failed' }, { status: 401 }) if not authenticated.
Proxy for request forwarding
Use proxy to rewrite requests to different paths using NextResponse.rewrite(). Check the request.nextUrl.pathname and rewrite to a new URL if conditions match. This allows intercepting and modifying requests before they reach route handlers.
Proxy for redirects
Use proxy to redirect requests using NextResponse.redirect(). Modify request.nextUrl.pathname and return NextResponse.redirect(request.nextUrl) to redirect the request to a new path, such as redirecting from /v1/docs to /v2/docs.
Security - header handling in Route Handlers
Be deliberate about where headers go. Upstream request headers: In Proxy, NextResponse.next({ request: { headers } }) modifies headers your server receives without exposing them to the client. Response headers: new Response(..., { headers }), NextResponse.json(..., { headers }), NextResponse.next({ headers }), or response.headers.set(...) send headers to the client. If sensitive values are appended to response headers, they will be visible to clients.
Rate limiting in Route Handlers
Implement rate limiting in Next.js Route Handlers by checking rate limits in code and returning 429 status with error message if rate limited. In addition to code-based checks, enable any rate limiting features provided by your hosting provider.
Verifying incoming request payloads
Never trust incoming request data. Validate content type and size, and sanitize against XSS before use. Use timeouts to prevent abuse and protect server resources. Store user-generated static assets in dedicated services. When possible, upload them from the browser and store the returned URI in your database to reduce request size.
Access to protected resources security
Always verify credentials before granting access. Do not rely on proxy alone for authentication and authorization. Remove sensitive or unnecessary data from responses and backend logs. Rotate credentials and API keys regularly.
Preflight requests and OPTIONS method
Preflight requests use the OPTIONS method to ask the server if a request is allowed based on origin, method, and headers. If OPTIONS is not defined in a Route Handler, Next.js adds it automatically and sets the Allow header based on the other defined methods.
Library pattern for Route Handlers
Community libraries often use the factory pattern for Route Handlers. A library exports a createHandler function that returns a handler object. This handler is then exported as GET, POST, or other HTTP methods. The library customizes behavior based on the method and pathname in the request.
Proxy factory pattern
Libraries can provide a proxy factory function like createMiddleware() that is exported as the default from a proxy.ts file. Third-party libraries may still refer to proxy as middleware.
Caveat: Server Components should not use Route Handlers for data fetching
Fetch data in Server Components directly from its source, not via Route Handlers. For Server Components prerendered at build time, using Route Handlers will fail the build step because there is no server listening for requests during build. For Server Components rendered on demand, fetching from Route Handlers is slower due to the extra HTTP round trip between the handler and the render process.
Server fetch requires absolute URLs
A server-side fetch request requires absolute URLs, implying an HTTP round trip to an external server. During development, the development server acts as the external server. At build time there is no server. At runtime, the server is available through the public-facing domain.
Data fetching needs best served by Server Components
Server Components cover most data-fetching needs. However, fetching data client-side might be necessary for data depending on client-only Web APIs (geo-location, storage, audio, file) or frequently polled data. For these cases, use community libraries like swr or react-query.
Server Actions limitations for data fetching
Server Actions let you run server-side code from the client and their primary purpose is to mutate data from the frontend. However, Server Actions are queued, so using them for data fetching introduces sequential execution and should be avoided.
Export mode Route Handler limitations
In export mode (static site without runtime server), only GET Route Handlers are supported in combination with the dynamic route segment config set to 'force-static'. This can be used to generate static HTML, JSON, TXT, or other files. Features requiring the Next.js runtime are not supported in export mode.
Export mode Route Handler example
In app/hello-world/route.ts, set export const dynamic = 'force-static' and define export function GET() { return new Response('Hello World', { status: 200 }) } to generate a static response file in export mode.
Deployment environment limitations for Route Handlers
Some hosts deploy Route Handlers as lambda functions, which means: Route Handlers cannot share data between requests. The environment may not support writing to File System. Long-running handlers may be terminated due to timeouts. WebSockets won't work because the connection closes on timeout or after the response is generated.