Blog

Using Middleware in Next.js

Update note: In Next.js 16, the middleware.ts file was renamed to proxy.ts and the middleware function to proxy. The concepts below (matchers, rewrites, cookie and header handling) still apply the same way.

What Is Middleware and When Do You Use It?

You might be picturing a scene like this: "A request leaves the browser and heads for the server. On the way, our middleware friend, holding a digital whistle, stops it: 'Hold on, where do you think you're going? Got ID? Are your headers in order?'"

That's exactly it!

In Next.js, middleware is a small but powerful piece of code that runs before a request is completed. Based on the incoming request, it can:

  • redirect the user to another page,
  • serve a different page while keeping the URL (rewrite),
  • add or remove headers,
  • or even respond directly.

The best part: middleware runs before the page cache and right before routes are matched. No matter how heavily a page is cached, middleware sees it, which makes it great for fast, lightweight work.

Use Cases

👣 Quick redirects: Reading a small part of the incoming request and sending the user somewhere else right away (for example, language-based redirects).

🧪 A/B tests: Routing users to different experiences.

📦 Header manipulation: Adding a custom header to every page (for example X-Robots-Tag).

🍪 Cookie manipulation: Remembering that a user prefers dark mode.

Writing Your First Middleware

/my-next-app
├── app/
├── public/
├── middleware.ts
..

Next.js supports only one middleware.ts (or .js) file. Even so, you can keep your middleware modular: split the logic into separate .ts or .js files and combine them in middleware.ts for a central, tidy structure. This avoids conflicts, keeps things simple and helps performance.

// middleware.ts
import type { NextRequest } from "next/server";
import { authMiddleware } from "./middlewares/auth";
import { experimentRedirect } from "./middlewares/abTesting";

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/admin')) {
    return authMiddleware(request);
  }

  return experimentRedirect(request);
}

Execution Order

Middleware runs in a specific order within a Next.js app, and it's worth keeping that order in mind while writing it.

  1. headers in next.config.js
  2. redirects in next.config.js
  3. …see the rest here: matching paths

This ensures middleware only runs on the paths that need it, avoiding performance loss and confusion.

Choosing Paths with a Matcher

The matcher option lets you decide which routes middleware runs on. You can define a single path or an array of paths:

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  return NextResponse.redirect(new URL('/home', request.url))
}

export const config = {
  matcher: ['/about/:path*', '/dashboard/:path*'],
}

Regular expressions are supported for more advanced matching. For example, you can use a "negative lookahead" to exclude certain paths:

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
  ],
}

Filtering with has and missing

Goal: make middleware run only when it's actually needed.

matcher: [
  {
    source: '/((?!api|_next|favicon.ico|sitemap.xml|robots.txt).*)',
    // Run when these headers are absent
    missing: [
      { type: 'header', key: 'next-router-prefetch' },
      { type: 'header', key: 'purpose', value: 'prefetch' },
    ],
  },
  {
    source: '/((?!api|_next|favicon.ico|sitemap.xml|robots.txt).*)',
    // Run when these headers are present
    has: [
      { type: 'header', key: 'next-router-prefetch' },
      { type: 'header', key: 'purpose', value: 'prefetch' },
    ],
  },
  {
    source: '/((?!api|_next|favicon.ico|sitemap.xml|robots.txt).*)',
    // Run when one header is present but another is missing
    has: [{ type: 'header', key: 'x-present' }],
    missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
  },
]

Conditional Rewrite

Goal: route requests based on what comes in. The request URL is checked against a pattern and matching paths are rewritten to a new URL. When a URL starting with /admin comes in, we serve the /internal/admin-panel page instead.

export function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith('/admin')) {
    return NextResponse.rewrite(new URL('/internal/admin-panel', request.url))
  }

  return NextResponse.next()
}

Cookie Management

Cookie operations (cookies.get/set/delete) come in handy for things like user sessions or theme preferences. The code below checks whether the user is logged in and redirects accordingly: if there's no auth_token cookie, the user is sent to /login. It also stores the last page the user visited in a last_visited cookie.

export function middleware(request: NextRequest) {
  const authToken = request.cookies.get('auth_token')

  if (!authToken) return NextResponse.redirect(new URL('/login', request.url))

  const response = NextResponse.next()
  response.cookies.set('last_visited', request.nextUrl.pathname)
  return response
}

// ...

request.cookies.get('auth_token') // Reads the cookie with the given key
response.cookies.set('auth_token', '123abc') // Creates a cookie or updates an existing one
request.cookies.delete('auth_token') // Deletes the cookie with the given key

Setting Request/Response Headers

We can set request and response headers through the API:

export function middleware(request: NextRequest) {
  // Copy the incoming request headers so we can modify them
  const requestHeaders = new Headers(request.headers)

  // Add a header describing the type of client making the request
  requestHeaders.set('x-client-type', 'internal-service')

  // Create a response with the new headers and pass the request on
  const response = NextResponse.next({
    request: {
      headers: requestHeaders, // Forward the new headers
    },
  })

  // Add a response header showing that middleware ran
  response.headers.set('x-middleware-trace', 'enabled')

  return response
}

Custom Responses from Middleware

This lets us handle request errors easily and send the user where they need to go.

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { isAuthenticated } from '@lib/auth'

export const config = {
  matcher: '/api/:function*',
}

export function middleware(request: NextRequest) {
  if (!isAuthenticated(request)) {
    // If authentication fails, return a JSON error with a 401 status
    return Response.json(
      { success: false, message: 'authentication failed' },
      { status: 401 }
    )
  }

  return NextResponse.next()
}

⚙️ Quick Notes

  • CORS handling: You can set Cross-Origin Resource Sharing (CORS) headers in middleware to control requests from other origins, including preflight (OPTIONS) requests and simple requests. CORS headers are critical for secure, controlled API access.
  • waitUntil and NextFetchEvent: The waitUntil() method lets middleware keep async work running in the background.
  • Advanced middleware flags:
// next.config.js
module.exports = {
  // Disables trailing slash redirects
  skipTrailingSlashRedirect: true,
  // Disables URL normalization and uses the original URL
  skipMiddlewareUrlNormalize: true,
}
// Code that controls adding a trailing slash to a URL
if (
  !pathname.endsWith('/') &&
  !pathname.match(/((?!\.well-known(?:\/.*)?)(?:[^/]+\/)*[^/]+\.\w+)/)
) {
  return NextResponse.redirect(
    new URL(`${req.nextUrl.pathname}/`, req.nextUrl)
  )
}
  • Unit testing (experimental): Since Next.js v15.1 you can unit test middleware, for example to check that it only runs for specific paths.
  • Edge vs. Node.js runtime: The Edge runtime is faster, while the Node.js runtime offers more flexibility (experimental).

🚀 Conclusion

Middleware in Next.js handles requests in one central place for tasks like redirects, authentication, and header and cookie management. Understanding how to use it well can significantly improve how data flows into and out of your project, which makes it a powerful, flexible tool for developers.

I enjoyed learning all this, and I hope sharing it helps you too.

Happy coding 👨‍💻 🎉


Originally published in Turkish on Medium.