How Middleware Works in Next.js 14 App Router?
Middleware in Next.js is a powerful feature that allows you to run code before a request is completed. This can be particularly useful for tasks such as authentication, rate limiting, and injecting headers. As of Next.js 14, middleware operates in the Edge runtime, offering performance benefits due to its proximity to the user. This tutorial will guide you through how middleware works, with a focus on using TypeScript.
What is Middleware?
In Next.js, middleware are functions that run before the request is completed. They allow you to manipulate requests and responses, perform checks, or mutate state before continuing with a standard request-response cycle. Then, based on the incoming request, you can modify the response by rewriting, redirecting, modifying the request or response headers, or responding directly. Middleware runs before cached content and routes are matched.
Middleware Naming and Placement
Middleware files should be named middleware.ts
or middleware.js
and be placed besides the app folder or in any specific directory under the /app
directory. This convention allows Next.js to recognize and execute these files at the appropriate stages of the request lifecycle.
- app/
- about/
- page.tsx
- middleware.ts
- contact/
- page.tsx
- middleware.ts
- middleware.ts
With src folder
-src
- app/
- about/
- page.tsx
- middleware.ts
- contact/
- page.tsx
- middleware.ts
- middleware.ts
Use Case
Checking User Session
Let's create a middleware to check if the user is authenticated by examining their session token.
Declare a Middleware in TypeScript
Create a middleware.ts
file in your app
directory or project root:
import { NextRequest, NextResponse } from 'next/server';
import jwt from 'jsonwebtoken';
import cookieparser from 'cookieparser';
// Secret key for JWT. Normally you should store this in environment variables.
const SECRET_KEY = process.env.SECRET_KEY || 'your-secret-key';
export async function middleware(req: NextRequest) {
const { cookies } = req;
const token = cookies.token;
if (!token) {
return NextResponse.redirect('/login');
}
try {
// Verify the JWT token
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded as any; // Add user info to request
return NextResponse.next();
} else {
return NextResponse.redirect('/login');
}
}
Other Possible Use Cases
Authentication and Authorization:
- Checking user roles
- Ensuring users are logged in before accessing restricted routes
Rate Limiting:
- Throttling requests to prevent abuse
A/B Testing:
- Serving different versions of the page based on cookies or headers
Localization:
- Redirecting users to region-specific pages based on their IP
Analytics:
- Collecting data about user requests
Skelton
Middleware functions are exported from a file named middleware.ts
or middleware.js
. Here is a skeleton in TypeScript:
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(req: NextRequest) {
// Logic goes here
return NextResponse.next();
}
Matchers in Middleware
Matchers restrict middleware execution to specified paths. Use the matcher
configuration in middleware.ts
:
Match only these paths
// by adding this export in the middleware, it configures middleware to execute only for these paths
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*'],
};
Match All except these
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
],
}
Bypass middleware for certain API's
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico, sitemap.xml, robots.txt (metadata files)
*/
{
source:
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
missing: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source:
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
has: [
{ type: 'header', key: 'next-router-prefetch' },
{ type: 'header', key: 'purpose', value: 'prefetch' },
],
},
{
source:
'/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)',
has: [{ type: 'header', key: 'x-present' }],
missing: [{ type: 'header', key: 'x-missing', value: 'prefetch' }],
},
],
}
Rules for Matchers
Configured matchers:
- MUST start with
/
- Can include named parameters:
/about/:path
matches/about/a
and/about/b
but not/about/a/c
- Can have modifiers on named parameters (starting with
:
):/about/:path*
matches/about/a/b/c
because*
is zero or more.?
is zero or one and+
one or more - Can use regular expression enclosed in parenthesis:
/about/(.*)
is the same as/about/:path*
Path Checking
How to check path in the middleware of next.js? You can use conditional logic to execute middleware based on specific conditions. Here's an example of checking the request path:
export async function middleware(req: NextRequest) {
if (req.nextUrl.pathname.startsWith('/admin')) {
// Logic for admin routes
} else {
// Logic for other routes
}
return NextResponse.next();
}
Rewrite and Redirect
Next.js provides the NextResponse
object to handle responses within middleware. You can use it to rewrite URLs, redirect requests, or send custom responses.
Rewrite Example:
You can use the NextResponse.rewrite
method to manipulate the request path without changing the URL in the browser:
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(req: NextRequest) {
if (req.nextUrl.pathname.startsWith('/old-path')) {
return NextResponse.rewrite(new URL('/new-path', req.url));
}
return NextResponse.next();
}
Redirect Example:
You can use the NextResponse.redirect
method to send users to a different URL completely:
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(req: NextRequest) {
if (!req.cookies.token) {
return NextResponse.redirect(new URL('/login', req.url));
}
return NextResponse.next();
}
Cookies in Middleware
Cookies are a common way to handle session data and user authentication. You can read cookies directly from the NextRequest
object and manipulate them accordingly.
Example: Reading and Setting Cookies
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(req: NextRequest) {
const token = req.cookies.get('token');
if (!token) {
const response = NextResponse.redirect(new URL('/login', req.url));
response.cookies.set('lastVisited', req.url);
return response;
}
// To set a cookie
const response = NextResponse.next();
response.cookies.set('visitedAt', new Date().toISOString());
return response;
}
Headers and CORS
How to set header and cors in the middleware.ts? You might need to set custom headers or manage Cross-Origin Resource Sharing (CORS) in your middleware.
Example: Setting Headers and Managing CORS
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(req: NextRequest) {
const response = NextResponse.next();
// Set custom headers
response.headers.set('X-Custom-Header', 'MyCustomValue');
// Handling CORS
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
return response;
}
Edge Runtime
One key aspect of Next.js middleware as of version 14 is that it runs in the Edge runtime instead of Node.js. This brings significant performance improvements because the Edge runtime is designed for low latency, distributed systems, and it runs closer to the user.
Key Differences
- Global Scope: Unlike Node.js, the Edge runtime doesn't have access to Node.js' global scope. For example, common modules like
fs
are not available. - Performance: The Edge runtime is optimized for speed and distributed execution, making it excellent for running middleware that handles routing, redirections, and simple logic fast.
Example:
// middleware.ts
import { NextRequest, NextResponse } from 'next/server';
export async function middleware(req: NextRequest) {
// Logic goes here
return NextResponse.next();
}
// Edge runtime: Fast and distributed
Conclusion
Middleware in Next.js 14 offers powerful, flexible tools for handling requests. Whether youโre validating sessions, setting headers, managing CORS, or redirecting based on conditions, middleware gives you the control you need to build robust web applications.
Summary
- Middleware Naming and Placement: Place
middleware.ts
ormiddleware.js
in the root or under specific directories in the/app
folder. - Use Cases: Authentication, rate limiting, A/B testing, localization, and analytics.
- Matchers: Utilize matchers to specify paths where middleware should be executed.
- Conditional Statements: Use conditional logic to handle different scenarios within the middleware.
- NextResponse: Use methods like
rewrite
andredirect
to handle request manipulations. - Cookies: Read, set, and manipulate cookies easily.
- Headers and CORS: Customize headers and manage CORS policies.
- Edge Runtime: Benefit from the performance improvements of running middleware on the Edge runtime.