Skip to content
·8 min read

Next.js Proxy and Middleware Patterns

Route a request in Next.js 16 and keep private data checks with the code that uses the data

Share

Suppose a studio moves its public opening hours from /sample-hours to /studio-hours. A request to the old address can send the visitor to the new one before the page loads. Proxy is useful when the choice depends on something in the incoming request, such as a preference already saved in a cookie.

A redirect can send someone to a sign-in page. The app still needs to check who may read or change a private booking when it handles that request. You can practice the routing part with two public addresses in a local app and decide the access rules separately on paper.

What does Next.js Proxy do?

Proxy can change where a matched request goes before Next.js reaches its page or request handler. The matcher is the list of address patterns that select those requests. Some rules in the app configuration, including fixed redirects, run earlier. Check both places if an address goes somewhere unexpected.

In Next.js 16, the current convention is one proxy.ts or proxy.js file alongside app or pages. If those folders are in src, put Proxy there too. Proxy uses Node.js and does not accept a file-level runtime setting. An older project may have middleware.ts and an export named middleware. Check the installed Next.js version and the old runtime before renaming anything. The Next.js 16 upgrade guide directs projects that still require the Edge runtime to keep the older Middleware convention while they depend on it. Proxy file reference · Next.js 16 upgrade guide

A static export serves files prepared during the build and cannot run Proxy when a request arrives. A Next.js server can run it, including in a container. If your host uses an adapter, check the adapter’s support for your version and feature before planning a live change. Check what your Next.js host can run

Where should your app check access?

If someone opens their bookings page while signed out, Proxy can optionally send them to sign in. A quick check can use session information that your authentication library knows how to verify. A cookie’s presence or format alone does not establish the account or its permissions. This early redirect helps people reach the right screen.

When the app reads or changes a booking, verify the session and check permission for that booking in server code close to the data. Put the shared check in the code that reads and writes records. Call it from each relevant page, request handler or Server Action. A Server Action is a server function that can handle a form submission. Each entry point must check the request even if the page hides the button or the Proxy matcher changes. If a sign-in flow accepts a return address, restrict it to the intended pages inside your app. Next.js authorization guidance

For a paper check, write down a pretend booking for Alex and a second account named Sam. A signed-out person gets no private booking data. Sam can sign in and still cannot read or change Alex’s booking. Alex can read it and can change it only if the app’s rules allow the change. Use the same three cases when you later verify an isolated test app.

How can you test one redirect locally?

A redirect tells the browser to request a different address. A rewrite serves a different route while the address in the browser stays the same. Use a disposable local Next.js 16 App Router project with TypeScript, a working home page and no existing Proxy, Middleware or redirect rules. Use no customer data, service credentials or live connections. You can also trace the two addresses on paper.

  1. Add app/studio-hours/page.tsx to the local project. Use src/app/studio-hours/page.tsx if the app lives in src.

    export default function StudioHours() {
      return <h1>Sample studio hours</h1>;
    }
    
  2. Add proxy.ts alongside the app folder. The rule only matches the old practice address.

    import { NextResponse, type NextRequest } from 'next/server';
    
    export function proxy(request: NextRequest) {
      return NextResponse.redirect(new URL('/studio-hours', request.url), 307);
    }
    
    export const config = {
      matcher: '/sample-hours',
    };
    
  3. Open /sample-hours on the local server. The address bar should change to /studio-hours and show “Sample studio hours.” In the browser’s Network panel, the old address should return a temporary 307 redirect and the new page should load. Open /studio-hours directly and confirm there is no redirect back. Check the project’s home page to confirm it still opens as before.

Next.js uses 307 for temporary and 308 for permanent redirects in the redirects setting. A rewrite is useful when the original address should stay visible, such as serving a different public landing page for a reader’s saved preference. Redirect settings · Redirect and rewrite behavior

Where should a request limit live?

If your app needs to limit requests, decide which action to limit and what should identify the person or request. A counter in the memory of one process can disappear on restart and cannot enforce the same limit across separate instances. For a live app, use a documented hosting control or a shared store that can keep the count across instances. Use request identity that your host or sign-in system documents as trustworthy. Check the rule against the specific action, including whether an ordinary user can continue after the limit resets. Self-hosting request controls

What if you offer a regional page?

Let people choose the right regional page and make the default usable without a location signal. Next.js 15 removed the geo and ip properties from NextRequest. If you use location supplied by your host as a suggestion, check the host’s documented behavior and test the missing-value case. Location does not tell your app which private records a person may access. NextRequest version history

How can two public page versions share one address?

A rewrite can select a version before the browser receives the page. If you save the choice in a cookie, check that a reload uses the same choice while the cookie is present. Test the default when it is missing, and verify that your hosting and caching rules keep responses for the two choices separate where needed. An HttpOnly cookie is available to the server. Browser scripts cannot read it, so measure the displayed version through a signal your measurement code is allowed to see. Proxy cookies · HttpOnly behavior

Which code should handle each job?

JobWhere to put it
Send every visitor from a fixed old address to its replacementA redirect rule in the app or hosting configuration.
Choose a route using information on the incoming requestA narrowly matched Proxy rule when a simpler setting cannot express the choice.
Load a booking or save a changeThe server code for the page, handler or action, using the shared permission and data checks.

If downstream code needs a request header, set it on the request passed upstream. A header set on the response goes back to the browser. Forward only what the downstream code needs and keep credentials, private account details and diagnostic traces out of public response headers. Request and response headers

What if several routing rules apply?

Keep one Proxy entry point and move larger rules into separate functions. Decide which rule should win when two match. For example, moving an old public address can take precedence over choosing a version of the replacement page. Check that each rule matches the paths you intended and leaves unrelated pages alone. A rule that returns a redirect or rewrite ends that branch, so check the order with one request that could meet both rules.

What should you check when routing surprises you?

Start with the exact address and what you expected to see. In the browser’s Network panel, check the response status, the final address and the page content. If the expected rule did not run, compare the incoming path with the matcher and check the app’s configured redirects. Check for a loop by opening the destination directly. Use local server logs for more detail and leave sensitive values out of them.

A separate address can still share live settings, so stay local or on paper unless you can confirm the preview uses sample records, test accounts and services that are test-only or turned off. Before a live change, repeat the matched and unmatched cases on an isolated preview that uses the intended hosting mode. Separately verify that the app’s data checks handle the signed-out account, the signed-in account without permission, and the account with permission. A correct redirect proves the route choice. The data checks prove who can read or change the record.

Written forDevelopers
PJ
Pranay Joshi

20+ years building products at scale. VP of Product & Engineering, startup founder, and AI coach. Helping dreamers turn ideas into reality with vibe coding.