How to Use Middleware in Nextjs

How to Use Middleware in Next.js Next.js has revolutionized the way developers build modern web applications by combining server-side rendering, static site generation, and client-side hydration into a seamless developer experience. One of its most powerful yet underutilized features is Middleware . Introduced in Next.js 12, Middleware allows developers to run code before a request is completed —

Nov 10, 2025 - 08:39
Nov 10, 2025 - 08:39
 1

How to Use Middleware in Next.js

Next.js has revolutionized the way developers build modern web applications by combining server-side rendering, static site generation, and client-side hydration into a seamless developer experience. One of its most powerful yet underutilized features is Middleware. Introduced in Next.js 12, Middleware allows developers to run code before a request is completed whether its a page render, API route, or static asset request. This enables powerful functionality like authentication, redirections, request modification, A/B testing, and geolocation-based content delivery all without touching your applications core logic.

Unlike traditional server-side solutions that require external proxies or complex backend infrastructure, Next.js Middleware runs at the edge meaning it executes close to the user, reducing latency and improving performance. Its written in JavaScript or TypeScript and lives in a special file named middleware.js (or middleware.ts) at the root of your project. This file is automatically detected by Next.js and runs on every request, giving you fine-grained control over the request lifecycle.

Whether youre building a global e-commerce platform that needs region-specific pricing, a SaaS application requiring authentication checks before every route, or a marketing site that redirects users based on device type, Middleware provides a clean, scalable, and performant solution. In this comprehensive guide, well walk you through everything you need to know to effectively use Middleware in Next.js from setup and configuration to advanced patterns and real-world use cases.

Step-by-Step Guide

Setting Up Middleware in Your Next.js Project

Before you can use Middleware, ensure youre running Next.js 12 or higher. You can check your version by running:

npm list next

If youre on an older version, upgrade using:

npm install next@latest

Once youve confirmed your Next.js version, create a new file at the root of your project directory:

  • For JavaScript: middleware.js
  • For TypeScript: middleware.ts

This file will contain all your Middleware logic. Its important to note that Middleware runs on every request including static assets like images, CSS, and JavaScript files so efficiency is critical. Avoid heavy computations or external API calls unless absolutely necessary.

Writing Your First Middleware Function

Middleware exports a function that receives a NextRequest object and a NextResponse object (or a NextFetchEvent if youre using the fetch API). Heres a minimal example:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

console.log('Middleware executed for:', request.url);

}

This function will log every URL accessed by a user. To make it do something useful, lets redirect users from the root path to a specific page:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

const { pathname } = request.nextUrl;

if (pathname === '/') {

return NextResponse.redirect(new URL('/home', request.url));

}

}

In this example, when a user visits /, theyre automatically redirected to /home. The NextResponse.redirect() method returns a response with a 307 status code by default, which preserves the HTTP method (GET, POST, etc.). You can also specify a different status code:

return NextResponse.redirect(new URL('/home', request.url), 301);

Configuring Middleware Matchers

By default, Middleware runs on every request even for static assets like /favicon.ico or /_next/static/.... This can lead to unnecessary performance overhead. To optimize, use the config object to specify which paths should trigger your Middleware.

Heres how to restrict Middleware to only run on specific routes:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

const { pathname } = request.nextUrl;

if (pathname.startsWith('/dashboard')) {

const token = request.cookies.get('auth_token')?.value;

if (!token) {

return NextResponse.redirect(new URL('/login', request.url));

}

}

}

export const config = {

matcher: ['/dashboard/:path*'],

};

The matcher array accepts strings or arrays of strings that define which paths should trigger the Middleware. Common patterns include:

  • ['/dashboard/:path*'] matches any path starting with /dashboard
  • ['/api/:path*'] matches all API routes
  • ['/((?!api|_next/static|_next/image|favicon.ico).*)'] excludes specific paths

The use of negative lookahead (?!...) is particularly useful when you want to exclude certain paths from Middleware execution. For example, you might want to avoid running Middleware on static assets to prevent unnecessary processing.

Accessing Request Data

Middleware gives you full access to the incoming request. You can read headers, cookies, URL parameters, and even the request body (in API routes). Heres how to extract common data:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

const userAgent = request.headers.get('User-Agent');

const locale = request.nextUrl.locale;

const token = request.cookies.get('session')?.value;

const path = request.nextUrl.pathname;

console.log({ userAgent, locale, token, path });

// Modify the request headers before forwarding

const requestHeaders = new Headers(request.headers);

requestHeaders.set('X-User-Agent', userAgent || 'unknown');

// Return a modified request

return NextResponse.next({

request: {

headers: requestHeaders,

},

});

}

By using NextResponse.next(), you can modify the request headers and continue the request lifecycle. This is useful for injecting metadata that your application components can later access via headers() in Server Components or API routes.

Modifying Responses

Middleware isnt limited to redirects or header modifications you can also return custom responses. For example, you can block requests based on IP geolocation or rate limiting:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

const ip = request.headers.get('x-forwarded-for') || request.ip;

const blockedIps = ['192.168.1.100', '10.0.0.5'];

if (blockedIps.includes(ip)) {

return NextResponse.json(

{ error: 'Access denied' },

{ status: 403 }

);

}

return NextResponse.next();

}

Here, were blocking specific IPs and returning a 403 Forbidden response. This is useful for security purposes, especially when combined with a service like Cloudflare that provides the real client IP via the x-forwarded-for header.

Using Middleware with API Routes

Middleware also applies to API routes. This means you can authenticate requests to /api/users or /api/orders without adding authentication logic to each endpoint individually.

export function middleware(request) {

const { pathname } = request.nextUrl;

if (pathname.startsWith('/api')) {

const authHeader = request.headers.get('Authorization');

if (!authHeader || !authHeader.startsWith('Bearer ')) {

return NextResponse.json(

{ error: 'Unauthorized' },

{ status: 401 }

);

}

}

}

Now, every API route in your application is protected by a single authentication check. This reduces code duplication and centralizes security logic.

Working with Cookies

Middleware is ideal for managing cookies setting, reading, or deleting them based on conditions. For example, you can set a cookie after a successful login redirect:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

const { pathname } = request.nextUrl;

if (pathname === '/login' && request.method === 'POST') {

const formData = await request.formData();

const email = formData.get('email');

if (email) {

const response = NextResponse.redirect(new URL('/dashboard', request.url));

response.cookies.set('auth_token', 'abc123', {

httpOnly: true,

secure: process.env.NODE_ENV === 'production',

maxAge: 60 * 60 * 24 * 7, // 7 days

path: '/',

});

return response;

}

}

return NextResponse.next();

}

Note: When setting cookies in Middleware, you must return a NextResponse object not just call cookies.set() on the request. The cookie is attached to the response that gets sent back to the client.

Dynamic Middleware with Fetch

Middleware can make HTTP requests using the built-in fetch API. This is useful for checking user permissions against an external service or validating tokens:

import { NextRequest, NextResponse } from 'next/server';

export async function middleware(request) {

const { pathname } = request.nextUrl;

if (pathname.startsWith('/admin')) {

const token = request.cookies.get('auth_token')?.value;

if (!token) {

return NextResponse.redirect(new URL('/login', request.url));

}

// Validate token with external API

const res = await fetch('https://api.yourservice.com/validate', {

headers: {

Authorization: Bearer ${token},

},

});

if (!res.ok) {

return NextResponse.redirect(new URL('/login', request.url));

}

const data = await res.json();

if (!data.isAdmin) {

return NextResponse.redirect(new URL('/unauthorized', request.url));

}

}

return NextResponse.next();

}

Because this function uses await, it must be declared as async. Be cautious with external calls they add latency. Consider caching responses or using a CDN edge cache if possible.

Best Practices

Keep Middleware Lightweight

Middleware runs on every matched request, including static assets. Heavy operations like database queries, complex computations, or multiple external API calls will slow down your entire application. Always optimize for speed.

Use matcher to restrict execution to only necessary paths. Avoid running Middleware on /_next/static, /favicon.ico, or image routes unless you have a specific reason.

Use Environment Variables for Configuration

Never hardcode sensitive values like API keys, allowed domains, or IP lists. Use environment variables:

export const config = {

matcher: [process.env.MIDDLEWARE_MATCHER || '/dashboard/:path*'],

};

And in your .env.local:

MIDDLEWARE_MATCHER=/dashboard/:path*

Test Middleware Thoroughly

Because Middleware affects every request, a small bug can break your entire application. Always test:

  • Redirects on different paths
  • Cookie behavior across sessions
  • Response headers in browser dev tools
  • API route protection

Use Next.jss built-in development server and simulate requests with tools like curl or Postman. For example:

curl -H "Cookie: auth_token=abc123" http://localhost:3000/dashboard

Avoid Infinite Redirect Loops

One of the most common mistakes is creating redirect loops. For example:

if (pathname === '/login') {

return NextResponse.redirect(new URL('/login', request.url)); // ? Infinite loop

}

Always check the current path before redirecting to it. Use a conditional guard:

if (pathname === '/login' && !token) {

return NextResponse.redirect(new URL('/login', request.url)); // ? Safe

}

if (pathname !== '/login' && !token) {

return NextResponse.redirect(new URL('/login', request.url)); // ? Safe

}

Use TypeScript for Type Safety

If youre using TypeScript, always type your Middleware function:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest): NextResponse | void {

// TypeScript ensures request has the correct shape

}

This prevents runtime errors and improves IDE autocomplete and linting.

Log Strategically

Use logging to debug Middleware, but avoid excessive logging in production. Use console.log only in development, and integrate with a logging service like LogRocket, Sentry, or Datadog in production.

Handle Edge Runtime Limitations

Middleware runs on the Edge Runtime by default, which has restrictions:

  • No Node.js core modules (e.g., fs, path)
  • No setTimeout or setInterval
  • Only a subset of Web APIs available

If you need full Node.js capabilities, you can opt out of the Edge Runtime by adding:

export const config = {

runtime: 'nodejs',

};

But this removes the performance benefits of edge execution. Only use this if absolutely necessary.

Tools and Resources

Official Documentation

The official Next.js documentation on Middleware is the most authoritative source:

It includes detailed explanations of the NextRequest and NextResponse APIs, matcher syntax, and Edge Runtime constraints.

VS Code Extensions

  • ESLint Helps catch syntax and logic errors in your Middleware code
  • Next.js Snippets Provides autocomplete for middleware.js boilerplate
  • Path Intellisense Helps with path matching patterns

Testing Tools

  • Playwright End-to-end testing for redirect and cookie behavior
  • Postman Manual testing of API route protections
  • curl Quick command-line testing of headers and cookies

Monitoring and Analytics

  • Sentry Track errors in Middleware execution
  • LogRocket Record user sessions to debug redirection issues
  • Cloudflare Analytics If deploying on Cloudflare, monitor edge execution metrics

Open Source Examples

Explore real-world implementations on GitHub:

Real Examples

Example 1: Language-Based Redirection

For a multilingual site, you can automatically redirect users based on their browser language:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

const { pathname } = request.nextUrl;

const acceptLanguage = request.headers.get('Accept-Language') || '';

const browserLang = acceptLanguage.split(',')[0]?.split('-')[0];

const supportedLangs = ['en', 'es', 'fr', 'de'];

const defaultLang = 'en';

// Don't redirect if already on a language path

if (supportedLangs.includes(pathname.split('/')[1])) {

return NextResponse.next();

}

// Redirect to preferred language

const preferredLang = supportedLangs.includes(browserLang) ? browserLang : defaultLang;

return NextResponse.redirect(new URL(/${preferredLang}${pathname}, request.url));

}

export const config = {

matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],

};

This ensures users see content in their native language without manually selecting it.

Example 2: A/B Testing with Cookies

Run an A/B test on your homepage by randomly assigning users to variant A or B:

import { NextRequest, NextResponse } from 'next/server';

export function middleware(request) {

const { pathname } = request.nextUrl;

if (pathname === '/') {

const abTestCookie = request.cookies.get('ab_test_variant');

const variant = abTestCookie ? abTestCookie.value : Math.random() > 0.5 ? 'a' : 'b';

const response = NextResponse.next();

response.cookies.set('ab_test_variant', variant, {

maxAge: 60 * 60 * 24 * 30, // 30 days

path: '/',

});

// Add variant to request headers for Server Components

response.headers.set('X-AB-Variant', variant);

return response;

}

return NextResponse.next();

}

export const config = {

matcher: ['/'],

};

Then, in your homepage component:

import { headers } from 'next/headers';

export default function Home() {

const headersList = headers();

const variant = headersList.get('X-AB-Variant');

return (

<div>

{variant === 'a' ? <h1>Welcome to Variant A</h1> : <h1>Welcome to Variant B</h1>}

</div>

);

}

Example 3: Rate Limiting by IP

Prevent abuse by limiting API requests per IP:

import { NextRequest, NextResponse } from 'next/server';

const requestCounts = new Map();

export function middleware(request) {

const ip = request.headers.get('x-forwarded-for') || request.ip;

const pathname = request.nextUrl.pathname;

// Only apply to API routes

if (!pathname.startsWith('/api')) return NextResponse.next();

const now = Date.now();

const windowMs = 60 * 1000; // 1 minute

// Initialize counter if not exists

if (!requestCounts.has(ip)) {

requestCounts.set(ip, { count: 0, timestamp: now });

}

const entry = requestCounts.get(ip);

// Reset counter if window expired

if (now - entry.timestamp > windowMs) {

entry.count = 0;

entry.timestamp = now;

}

entry.count++;

// Block if over limit (100 requests per minute)

if (entry.count > 100) {

return NextResponse.json(

{ error: 'Rate limit exceeded' },

{ status: 429 }

);

}

return NextResponse.next();

}

export const config = {

matcher: ['/api/:path*'],

};

Note: This is a basic in-memory implementation. For production, use Redis or another persistent store.

Example 4: Geo-Based Content Delivery

Use a geolocation API to serve region-specific content:

import { NextRequest, NextResponse } from 'next/server';

export async function middleware(request) {

const { pathname } = request.nextUrl;

// Only apply to homepage

if (pathname !== '/') return NextResponse.next();

// Get user's country from IP

const ip = request.headers.get('x-forwarded-for') || request.ip;

const geoRes = await fetch(https://ipapi.co/${ip}/json/);

const geoData = await geoRes.json();

const country = geoData.country_name;

// Set country in cookie and header

const response = NextResponse.next();

response.cookies.set('user_country', country, { maxAge: 60 * 60 * 24 });

response.headers.set('X-User-Country', country);

return response;

}

export const config = {

matcher: ['/'],

};

Then in your component:

import { headers } from 'next/headers';

export default function Home() {

const headersList = headers();

const country = headersList.get('X-User-Country');

return (

<div>

<h1>Welcome to {country || 'our site'}!</h1>

{country === 'United States' && <p>Special US pricing applies!</p>}

</div>

);

}

FAQs

What is the difference between Middleware and API Routes in Next.js?

Middleware runs before any request is processed including static assets and API routes and is designed for lightweight logic like redirection, header modification, and authentication. API Routes, on the other hand, are endpoints that handle specific HTTP requests and return data. Middleware is ideal for cross-cutting concerns, while API Routes are for application-specific data handling.

Can Middleware access the request body?

Yes, but only for API routes and POST/PUT requests. For non-API routes, the request body is not available due to performance constraints. You can access it using await request.json() or await request.text() but be cautious, as this consumes the body stream.

Does Middleware work with static export (next export)?

No. Middleware requires a server to execute and is incompatible with static exports. If youre using next export, you must use client-side routing or a different solution like a CDN-level redirect.

Can I use environment variables in Middleware?

Yes. You can access environment variables using process.env.VAR_NAME. However, only variables prefixed with NEXT_PUBLIC_ are exposed to the browser. For server-side variables (like API keys), use regular environment variables theyre safe in Middleware since it runs server-side.

Is Middleware faster than server-side authentication in API routes?

Yes. Because Middleware runs at the edge, it executes closer to the user, reducing latency. Server-side authentication in API routes requires a full request-response cycle through your server, which adds delay. Middleware is also more scalable since it doesnt require spawning Node.js processes.

How do I debug Middleware?

Use console.log() in development logs appear in the terminal where you ran npm run dev. For production, integrate with a logging service like Sentry. You can also use browser dev tools to inspect response headers and cookies to verify Middleware behavior.

Can I use Middleware with App Router and Pages Router?

Yes. Middleware works with both the App Router and Pages Router in Next.js 13+. The file structure and syntax are the same. However, the App Router provides more granular control with route groups and nested layouts, so you can place Middleware files in specific directories for scoped behavior.

What happens if Middleware throws an error?

If Middleware throws an unhandled error, Next.js will return a 500 Internal Server Error to the client. Always wrap external calls in try-catch blocks and return a safe response on failure.

Conclusion

Middleware in Next.js is a game-changing feature that brings server-side logic to the edge, enabling developers to build high-performance, secure, and personalized web applications with minimal overhead. From simple redirects to complex authentication flows and geo-targeted content, Middleware eliminates the need for external proxies or bloated backend services.

By following the best practices outlined in this guide using matcher patterns, keeping logic lightweight, leveraging environment variables, and testing thoroughly you can harness Middleware to its full potential. Real-world examples like language redirection, A/B testing, rate limiting, and geolocation-based delivery demonstrate how Middleware can solve common problems elegantly and efficiently.

As Next.js continues to evolve, Middleware will become even more integral to modern web development. Whether youre building a global SaaS platform or a multilingual marketing site, Middleware gives you the tools to deliver a seamless, secure, and scalable user experience all without sacrificing performance.

Start experimenting with Middleware today. Integrate it into your next project, measure the impact, and unlock the full power of edge computing in your Next.js applications.