Skip to main content

Next.js Middleware

In web development, middleware refers to code that runs between receiving a request and sending a response. Next.js Middleware allows you to execute code before a request is completed, giving you powerful control over how your application responds to requests.

Introduction to Next.js Middleware

Middleware enables you to run code before a request is completed. It executes on the Edge Runtime, which means it runs closest to your users, making it perfect for performance-critical tasks like:

  • Authentication & authorization
  • Bot protection
  • Redirects and rewrites
  • A/B testing
  • Internationalization (i18n) routing
  • Response headers manipulation
  • Feature flags based on user data

Let's explore how to implement and use middleware in your Next.js applications.

Creating Your First Middleware

To create a middleware in Next.js, you need to add a file called middleware.js (or middleware.ts if you're using TypeScript) in the root of your project.

Here's a simple middleware example:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// This function can be marked `async` if using `await` inside
export function middleware(request: NextRequest) {
console.log('Middleware executed!');
return NextResponse.next();
}

In this basic example, the middleware logs a message and allows the request to continue by returning NextResponse.next().

Middleware Execution

The middleware runs for every route in your application by default. When a user visits your site, the following sequence occurs:

  1. Next.js receives the request
  2. Middleware executes before the route is resolved
  3. Based on your middleware logic, the request can be:
    • Allowed to continue (NextResponse.next())
    • Redirected (NextResponse.redirect())
    • Rewritten to another destination (NextResponse.rewrite())
    • Responded to directly (return a NextResponse)

Response Manipulation

Redirects

You can use middleware to redirect users based on specific conditions:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Redirect users accessing /dashboard to login if not authenticated
const authCookie = request.cookies.get('authToken');

if (!authCookie && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}

return NextResponse.next();
}

Rewrites

You can rewrite the URL internally while keeping the original URL in the browser:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Show a maintenance page for specific paths
if (request.nextUrl.pathname.startsWith('/api/')) {
return NextResponse.rewrite(new URL('/maintenance', request.url));
}

return NextResponse.next();
}

Headers Manipulation

You can set, modify, or remove headers from the request:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Clone the request headers
const response = NextResponse.next();

// Set a new header
response.headers.set('x-custom-header', 'hello-world');

// Set security headers
response.headers.set('X-Frame-Options', 'DENY');
response.headers.set('X-Content-Type-Options', 'nosniff');
response.headers.set('Referrer-Policy', 'origin-when-cross-origin');

return response;
}

Matching Paths

Instead of running middleware for all routes, you can specify which paths it should run on:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Middleware logic here
return NextResponse.next();
}

// Specify which paths this middleware should run on
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
};

The matcher configuration accepts an array of path patterns:

  • :path* matches any number of path segments
  • :path+ matches one or more path segments
  • :path? matches zero or one path segment

Cookies Management

Next.js middleware provides a convenient way to work with cookies:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
// Get cookies
const theme = request.cookies.get('theme')?.value;

const response = NextResponse.next();

// Set a cookie
response.cookies.set('visited', 'true');

// Delete a cookie
if (theme === 'deprecated-theme') {
response.cookies.delete('theme');
}

return response;
}

Real-World Applications

Authentication Middleware

Here's a practical example of authentication middleware:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// Protected routes that require authentication
const protectedRoutes = ['/dashboard', '/profile', '/settings'];

export function middleware(request: NextRequest) {
const authToken = request.cookies.get('authToken')?.value;
const currentPath = request.nextUrl.pathname;

// Check if the route is protected and user is not authenticated
const isProtectedRoute = protectedRoutes.some(route =>
currentPath === route || currentPath.startsWith(`${route}/`)
);

if (isProtectedRoute && !authToken) {
const loginUrl = new URL('/login', request.url);

// Store the original URL to redirect after login
loginUrl.searchParams.set('returnTo', currentPath);

return NextResponse.redirect(loginUrl);
}

return NextResponse.next();
}

Internationalization (i18n) Middleware

Here's how you can implement language detection and redirection:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// Supported languages
const languages = ['en', 'es', 'fr', 'de'];
const defaultLanguage = 'en';

export function middleware(request: NextRequest) {
// Get the pathname
const { pathname } = request.nextUrl;

// Check if the pathname already includes a language
const pathnameHasLanguage = languages.some(
language => pathname.startsWith(`/${language}/`) || pathname === `/${language}`
);

if (pathnameHasLanguage) return NextResponse.next();

// Get the preferred language from the request headers
const acceptLanguage = request.headers.get('accept-language') || '';
const preferredLanguage = acceptLanguage
.split(',')
.map(lang => lang.split(';')[0].trim())
.find(lang => languages.includes(lang.substring(0, 2))) || defaultLanguage;

// Redirect to the preferred language
return NextResponse.redirect(
new URL(`/${preferredLanguage}${pathname}`, request.url)
);
}

// Only trigger middleware on specific paths
export const config = {
matcher: [
// Exclude files with extensions (images, static files, etc.)
'/((?!api|_next/static|_next/image|favicon.ico|.*\\.).*)',
],
};

A/B Testing Middleware

You can implement simple A/B testing using middleware:

typescript
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
const response = NextResponse.next();

// Get the existing A/B test cookie or create one
let testGroup = request.cookies.get('ab-test-group')?.value;

// If no test group exists, randomly assign one
if (!testGroup) {
testGroup = Math.random() < 0.5 ? 'A' : 'B';
response.cookies.set('ab-test-group', testGroup);
}

// Add the test group to headers for server components to access
response.headers.set('x-ab-test-group', testGroup);

// Optionally rewrite to different pages based on test group
if (request.nextUrl.pathname === '/pricing' && testGroup === 'B') {
return NextResponse.rewrite(new URL('/pricing-experiment', request.url));
}

return response;
}

Edge and Limitations

Next.js Middleware runs on the Edge Runtime, which is more limited than Node.js. Some important limitations to be aware of:

  • You can't access the filesystem
  • You can't use Node.js APIs
  • There are limited NPM packages that work in the Edge Runtime
  • The code bundle size must remain small

For a complete list of Edge Runtime limitations, refer to the Next.js documentation.

Summary

Next.js Middleware is a powerful feature that allows you to execute code before a request is completed. It runs at the edge, close to your users, making it perfect for:

  • Authentication and authorization flows
  • Custom routing logic
  • Headers manipulation
  • A/B testing
  • Internationalization
  • Feature flags and progressive rollouts

To create middleware:

  1. Add a middleware.ts file to the root of your project
  2. Export a middleware function that accepts a request parameter
  3. Return an appropriate response using the NextResponse API
  4. Optionally, specify path matchers to limit where middleware runs

Middleware enables you to add powerful server-side logic to your Next.js applications without compromising performance.

Additional Resources

Exercises

  1. Basic Middleware: Create a middleware that logs the path of every request.
  2. Protected Routes: Implement middleware that redirects unauthenticated users from protected routes.
  3. Device Detection: Create middleware that detects mobile devices and redirects them to a mobile-optimized version.
  4. Rate Limiting: Implement a simple rate limiter middleware that limits the number of requests from a single IP.
  5. Dark Mode Toggle: Use middleware to implement a site-wide dark mode toggle using cookies.

By completing these exercises, you'll gain practical experience with Next.js Middleware and be able to implement complex server-side logic in your applications.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)