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:
// 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:
- Next.js receives the request
- Middleware executes before the route is resolved
- 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
)
- Allowed to continue (
Response Manipulation
Redirects
You can use middleware to redirect users based on specific conditions:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
- Add a
middleware.ts
file to the root of your project - Export a middleware function that accepts a request parameter
- Return an appropriate response using the NextResponse API
- 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
- Basic Middleware: Create a middleware that logs the path of every request.
- Protected Routes: Implement middleware that redirects unauthenticated users from protected routes.
- Device Detection: Create middleware that detects mobile devices and redirects them to a mobile-optimized version.
- Rate Limiting: Implement a simple rate limiter middleware that limits the number of requests from a single IP.
- 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! :)