Next.js Middleware
Updated on
Middleware in Next.js allows developers to run code before a request reaches a page or route handler. It acts like a request filter that sits between the user request and the application, providing a powerful way to handle cross-cutting concerns in your application.
Middleware executes in an Edge Runtime environment, making it extremely fast and ideal for performance-sensitive operations. It can be used for various purposes:
- Authentication & Authorization: Verify user identity and permissions before accessing protected resources
- Route Protection: Restrict access to specific routes based on user roles or credentials
- Intelligent Redirects: Route users to appropriate pages based on location, device, or other criteria
- Request Logging: Log all incoming requests for monitoring and debugging
- A/B Testing: Route users to different versions of your application
- Localization: Detect user locale and serve localized content
- Security Headers: Add security headers to protect against common vulnerabilities
- Rate Limiting: Control request rate to prevent abuse
Middleware runs before rendering happens and operates on the Edge, which makes it extremely powerful and performant compared to traditional server-side middleware.
How middleware works
Middleware in Next.js is a function that runs before a request reaches a page or route handler. It acts like a request filter that sits between the user request and the application.
When a request is received, it first goes through the middleware functions before reaching the final destination.
What middleware does
- Allow the request
- Modify the request
- Redirect the request
- Block the request
Where middleware is defined
Middleware is defined in a file named `middleware.ts` or `middleware.js` at the root of the project or inside the `app` directory.
project/
├ app/
├ middleware.ts
├ package.json
The file name must be `middleware.ts` or `middleware.js`. Next JS will automatically detect and use the middleware file.
Basic Middleware Example
Here is a simple example of a middleware function:
import { NextResponse } from "next/server"
import { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
console.log("Middleware executed")
return NextResponse.next()
}
// Only run this middleware for /about (and any nested routes).
// export const config = { matcher: ["/about/:path*"] };
In the example above, the middleware function logs a message to the console and then calls `NextResponse.next()` to continue processing the request.
This will run every time a request is received.
export const config = { matcher: ["/about/:path*"] }; will only run the middleware for the /about route and any nested routes.
NextRequest
The `NextRequest` object contains information about the incoming request, such as headers, URL, and query parameters.
It is passed as an argument to the middleware function. The name of the parameter is `request` or `req`.
export function middleware(request: NextRequest) {
const url = request.nextUrl.pathname
console.log(url)
return NextResponse.next()
}
What nextRequest can access
- URL path
- Headers
- Cookies
- Query Parameters
Protecting Routes with Middleware
You can use middleware to protect routes by checking if the user is authenticated before allowing access to certain pages. For example:
import { NextResponse } from "next/server"
import type { NextRequest } from "next/server"
export function middleware(request: NextRequest) {
const token = request.cookies.get("token");
if (!token) {
return NextResponse.redirect(
new URL("/login", request.url)
)
}
return NextResponse.next()
}
If the token does not exist, the user is redirected to the login page.
Running Middleware for Specific Routes
We can specify which routes the middleware should run on using the `config` object. For example
export const config = {
matcher: "/dashboard/:path*",
}
Multiple Route Matchers
You can specify multiple routes for the middleware to run on by providing an array of matchers.
export const config = {
matcher: ["/dashboard/:path*", "/profile/:path*"],
}
Modify Response Headers
You can modify the response headers using the `NextResponse` object. For example, to add a custom header:
export function middleware(request: NextRequest) {
const response = NextResponse.next()
response.headers.set("x-custom-header", "hello")
return response
}In the example above, we create a response object using `NextResponse.next()`, then we set a custom header `x-custom-header` with the value `hello`, and finally we return the modified response.
Real-World Middleware Use Cases
1. Authentication & Token Verification
Verify JWT tokens or session cookies before allowing access to protected routes.
export function middleware(request: NextRequest) {
const token = request.cookies.get("auth_token")?.value;
if (!token && request.nextUrl.pathname.startsWith("/dashboard")) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/dashboard/:path*", "/api/protected/:path*"],
};
2. Rate Limiting
Limit request frequency to prevent abuse and DDoS attacks.
const requestCounts = new Map();
const RATE_LIMIT = 100; // requests
const TIME_WINDOW = 60000; // 1 minute
export function middleware(request: NextRequest) {
const ip = request.ip || "unknown";
const now = Date.now();
if (!requestCounts.has(ip)) {
requestCounts.set(ip, []);
}
const timestamps = requestCounts.get(ip) || [];
const recentRequests = timestamps.filter(
(t) => now - t < TIME_WINDOW
);
if (recentRequests.length >= RATE_LIMIT) {
return NextResponse.json(
{ error: "Too many requests" },
{ status: 429 }
);
}
recentRequests.push(now);
requestCounts.set(ip, recentRequests);
return NextResponse.next();
}
3. Localization & Language Detection
Route users to localized versions of your site based on their language preference.
export function middleware(request: NextRequest) {
const pathname = request.nextUrl.pathname;
// Check if locale is already in pathname
const locales = ["en", "es", "fr", "de"];
const hasLocale = locales.some(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
);
if (hasLocale) return NextResponse.next();
// Get user's preferred language from Accept-Language header
const preferredLocale =
request.headers.get("accept-language")?.split(",")[0].split("-")[0] || "en";
return NextResponse.redirect(
new URL(`/${preferredLocale}${pathname}`, request.url)
);
}
export const config = {
matcher: ["/((?!_next|api).*)"],
};
4. Security Headers
Add security headers to protect against common web vulnerabilities.
export function middleware(request: NextRequest) {
const response = NextResponse.next();
// Prevent clickjacking
response.headers.set("X-Frame-Options", "DENY");
// Prevent MIME type sniffing
response.headers.set("X-Content-Type-Options", "nosniff");
// Enable XSS protection
response.headers.set("X-XSS-Protection", "1; mode=block");
// Content Security Policy
response.headers.set(
"Content-Security-Policy",
"default-src 'self'; script-src 'self' 'unsafe-inline'"
);
return response;
}
5. A/B Testing
Route users to different versions for experimentation and testing.
export function middleware(request: NextRequest) {
const ab_test = request.cookies.get("ab_test")?.value || "control";
if (request.nextUrl.pathname === "/features/new") {
if (ab_test === "variant") {
request.nextUrl.pathname = "/features/new-variant";
}
}
return NextResponse.rewrite(request.nextUrl);
}
Middleware Best Practices
1. Keep Middleware Fast
- Middleware runs on the Edge, so minimize processing time
- Avoid heavy computations or database queries
- Cache authentication tokens or session data when possible
2. Use Matcher Wisely
- Specify matcher patterns to run middleware only on necessary routes
- Running on all routes increases latency for external requests
- Exclude static assets and API routes that don't need middleware
export const config = {
matcher: [
// Run on dashboard routes
"/dashboard/:path*",
// Run on API routes requiring auth
"/api/auth/:path*",
// Exclude specific paths
"/((?!public|_next/static|_next/image|favicon.ico).*)",
],
};
3. Handle Errors Gracefully
- Always return a valid Response object
- Provide meaningful error messages
- Avoid exposing sensitive information in error responses
4. Log Middleware Activity
Implement logging to monitor middleware behavior and debug issues.
export function middleware(request: NextRequest) {
const start = Date.now();
const response = NextResponse.next();
const duration = Date.now() - start;
console.log({
path: request.nextUrl.pathname,
method: request.method,
duration: `${duration}ms`,
timestamp: new Date().toISOString(),
});
return response;
}
5. Use TypeScript for Type Safety
- Use Next.js types for `NextRequest` and `NextResponse`
- Define interfaces for custom request/response properties
- Catch type errors at development time
Common Pitfalls to Avoid
1. Running Expensive Operations
✗ Don't: Make database calls or complex computations in middleware
// AVOID: This is too slow
export async function middleware(request: NextRequest) {
const user = await db.query("SELECT * FROM users..."); // Too slow!
return NextResponse.next();
}
✓ Do: Cache data or use lightweight checks
// BETTER: Use cached token validation
export function middleware(request: NextRequest) {
const token = request.cookies.get("auth_token")?.value;
if (!token) {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
2. Ignoring Matcher Performance
Middleware without matcher patterns runs on every request, including static assets. Always specify matchers for critical routes only.
3. Not Handling Redirects Properly
Using `NextResponse.redirect()` multiple times can create redirect loops. Test redirect chains carefully.
4. Exposing Sensitive Information
Never log or expose API keys, passwords, or user data in middleware logs or error responses.
5. Blocking Valid Requests
Be careful with authentication checks - ensure valid tokens are properly recognized and don't accidentally block legitimate users.