In previous modules, we learned the basics of authentication, connecting external providers, session management, and implementation of the role and permission system. Now we will focus on middleware - a powerful Next.js mechanism that allows executing code before reaching the target request, which is an ideal place to implement route protection.
Middleware in Next.js works at the server level but is executed before page rendering or API request handling. This enables centralized access management, redirecting, header modification, and many other operations.
Let's start by understanding how middleware works in Next.js:
middleware.ts file (or middleware.js) in the main directory of the Next.js project.Simple middleware example:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 console.log('Middleware executed for:', request.nextUrl.pathname);
7
8 // Continue normal request processing
9 return NextResponse.next();
10}
11
12// Apply middleware only to specific paths
13export const config = {
14 matcher: ['/dashboard/:path*', '/api/:path*'],
15};Now let's move on to the full implementation of middleware for route protection in a Next.js application.
Let's start with simple middleware that checks if the user is logged in and protects specific paths:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6export async function middleware(request: NextRequest) {
7 // Get the session token from the cookie
8 const token = await getToken({ req: request });
9
10 // Check if user is logged in
11 const isLoggedIn = !!token;
12
13 // Current path
14 const { pathname } = request.nextUrl;
15
16 // Protected paths - requiring login
17 const protectedPaths = [
18 '/dashboard',
19 '/profile',
20 '/settings',
21 ];
22
23 // Paths only for unauthenticated users (e.g. login page)
24 const authPaths = ['/auth/login', '/auth/register'];
25
26 // Check if the current path is protected
27 const isProtectedPath = protectedPaths.some(path =>
28 pathname === path || pathname.startsWith(`${path}/`)
29 );
30
31 // Check if the current path is an authentication page
32 const isAuthPath = authPaths.some(path => pathname === path);
33
34 // If the user tries to access a protected path but is not logged in
35 if (isProtectedPath && !isLoggedIn) {
36 // Redirect to the login page with information about the target path
37 const url = new URL('/auth/login', request.url);
38 url.searchParams.set('callbackUrl', encodeURI(pathname));
39 return NextResponse.redirect(url);
40 }
41
42 // If the user is logged in and tries to access the login/registration page
43 if (isAuthPath && isLoggedIn) {
44 // Redirect to the dashboard
45 return NextResponse.redirect(new URL('/dashboard', request.url));
46 }
47
48 // In other cases, continue normal processing
49 return NextResponse.next();
50}
51
52// Specify for which paths the middleware should run
53export const config = {
54 matcher: [
55 /*
56 * Match all paths except:
57 * 1. Static files (e.g. /favicon.ico, /images/*, etc.)
58 * 2. next-auth API Route (/api/auth/*, which handles authentication itself)
59 */
60 '/((?!api/auth|_next/static|_next/image|favicon.ico).*)',
61 ],
62};Let's extend our middleware to also include user roles and permissions:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6// Structure describing access requirements for paths
7interface PathConfig {
8 requireAuth: boolean; // Whether login is required
9 roles?: string[]; // Allowed roles (if empty, all roles have access)
10 permissions?: string[]; // Required permissions
11}
12
13// Path access configuration
14const pathAccessConfig: Record<string, PathConfig> = {
15 // Administrative paths
16 '/admin': {
17 requireAuth: true,
18 roles: ['admin'],
19 },
20
21 // Moderator paths
22 '/moderator': {
23 requireAuth: true,
24 roles: ['admin', 'moderator'],
25 },
26
27 // Dashboard paths - require login
28 '/dashboard': {
29 requireAuth: true,
30 },
31
32 // User management page - requires user:manage permission
33 '/admin/users': {
34 requireAuth: true,
35 roles: ['admin'],
36 permissions: ['user:manage'],
37 },
38
39 // Public paths - available to everyone
40 '/': {
41 requireAuth: false,
42 },
43 '/about': {
44 requireAuth: false,
45 },
46 '/contact': {
47 requireAuth: false,
48 },
49
50 // Authentication paths - available only to unauthenticated
51 '/auth/login': {
52 requireAuth: false,
53 },
54 '/auth/register': {
55 requireAuth: false,
56 },
57};
58
59// Helper function to check whether a path matches a configuration
60function matchPathConfig(pathname: string): [string, PathConfig] | null {
61 // Check exact match
62 if (pathAccessConfig[pathname]) {
63 return [pathname, pathAccessConfig[pathname]];
64 }
65
66 // Check prefix match
67 for (const configPath in pathAccessConfig) {
68 // Ignore exact matches since we already checked them
69 if (configPath === pathname) continue;
70
71 // Check if the current path starts with the configured path + /
72 if (pathname.startsWith(`${configPath}/`)) {
73 return [configPath, pathAccessConfig[configPath]];
74 }
75 }
76
77 // No match
78 return null;
79}
80
81export async function middleware(request: NextRequest) {
82 // Get the session token from the cookie
83 const token = await getToken({ req: request });
84
85 // Current path
86 const { pathname } = request.nextUrl;
87
88 // Find access configuration for the current path
89 const pathMatch = matchPathConfig(pathname);
90
91 // If no configuration was found for this path, continue normal processing
92 if (!pathMatch) {
93 return NextResponse.next();
94 }
95
96 const [configPath, config] = pathMatch;
97
98 // Authentication pages - redirect to dashboard if the user is already logged in
99 if (
100 !config.requireAuth &&
101 (pathname === '/auth/login' || pathname === '/auth/register') &&
102 token
103 ) {
104 return NextResponse.redirect(new URL('/dashboard', request.url));
105 }
106
107 // If the path requires authentication and the user is not logged in
108 if (config.requireAuth && !token) {
109 // Redirect to the login page with information about the target path
110 const url = new URL('/auth/login', request.url);
111 url.searchParams.set('callbackUrl', pathname);
112 return NextResponse.redirect(url);
113 }
114
115 // If the path requires specific roles, check the user's permissions
116 if (token && config.roles && config.roles.length > 0) {
117 const userRoles = token.role as string;
118
119 if (!config.roles.includes(userRoles)) {
120 // User does not have the required role - redirect to the no-permissions page
121 return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
122 }
123 }
124
125 // If the path requires specific permissions, check the user's permissions
126 if (token && config.permissions && config.permissions.length > 0) {
127 const userPermissions = token.permissions as string[] || [];
128
129 // Check if the user has all required permissions
130 const hasAllPermissions = config.permissions.every(permission =>
131 userPermissions.includes(permission)
132 );
133
134 if (!hasAllPermissions) {
135 // User does not have the required permissions - redirect to the no-permissions page
136 return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
137 }
138 }
139
140 // All conditions met - continue
141 return NextResponse.next();
142}
143
144// Match for all paths except static files and authentication API
145export const config = {
146 matcher: [
147 '/((?!api/auth|_next/static|_next/image|images|favicon.ico).*)',
148 ],
149};As you can see, the described middleware works for pages, redirecting the user in case of missing permissions. For API routes, we should return appropriate error codes rather than redirecting the user. Here is the middleware implementation specifically for API:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6// API path access configuration
7const apiAccessConfig: Record<string, {
8 roles?: string[];
9 permissions?: string[];
10}> = {
11 // Administrator API
12 '/api/admin': {
13 roles: ['admin'],
14 },
15
16 // Moderator API
17 '/api/moderator': {
18 roles: ['admin', 'moderator'],
19 },
20
21 // User management API
22 '/api/users': {
23 permissions: ['user:read'],
24 },
25 '/api/users/create': {
26 permissions: ['user:create'],
27 },
28 '/api/users/update': {
29 permissions: ['user:update'],
30 },
31 '/api/users/delete': {
32 permissions: ['user:delete'],
33 },
34};
35
36// Helper function to check whether an API path matches a configuration
37function matchApiConfig(pathname: string) {
38 // Check exact match
39 if (apiAccessConfig[pathname]) {
40 return [pathname, apiAccessConfig[pathname]];
41 }
42
43 // Check prefix match
44 for (const configPath in apiAccessConfig) {
45 if (pathname.startsWith(`${configPath}/`)) {
46 return [configPath, apiAccessConfig[configPath]];
47 }
48 }
49
50 return null;
51}
52
53export async function middleware(request: NextRequest) {
54 // Get the session token
55 const token = await getToken({ req: request });
56
57 // Current path
58 const { pathname } = request.nextUrl;
59
60 // Check if this is an API request
61 const isApiRequest = pathname.startsWith('/api/');
62
63 // If it is not API, proceed to the next middleware
64 if (!isApiRequest) {
65 return NextResponse.next();
66 }
67
68 // Skip authentication routes
69 if (pathname.startsWith('/api/auth/')) {
70 return NextResponse.next();
71 }
72
73 // If it is API and the user is not logged in
74 if (!token) {
75 return NextResponse.json(
76 { error: 'Unauthorized - Authentication required' },
77 { status: 401 }
78 );
79 }
80
81 // Find access configuration for this API endpoint
82 const apiMatch = matchApiConfig(pathname);
83
84 // If no configuration, we assume being logged in is sufficient
85 if (!apiMatch) {
86 return NextResponse.next();
87 }
88
89 const [configPath, config] = apiMatch;
90
91 // Check user role
92 if (config.roles && config.roles.length > 0) {
93 const userRoles = token.role as string;
94
95 if (!config.roles.includes(userRoles)) {
96 return NextResponse.json(
97 { error: 'Forbidden - Insufficient role permissions' },
98 { status: 403 }
99 );
100 }
101 }
102
103 // Check user permissions
104 if (config.permissions && config.permissions.length > 0) {
105 const userPermissions = token.permissions as string[] || [];
106
107 const hasAllPermissions = config.permissions.every(permission =>
108 userPermissions.includes(permission)
109 );
110
111 if (!hasAllPermissions) {
112 return NextResponse.json(
113 { error: 'Forbidden - Insufficient permissions' },
114 { status: 403 }
115 );
116 }
117 }
118
119 // All conditions met
120 return NextResponse.next();
121}
122
123// Match for all API routes
124export const config = {
125 matcher: ['/api/:path*'],
126};We can combine both approaches in a single middleware:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { getToken } from 'next-auth/jwt';
5
6// Page access configuration
7const pageAccessConfig: Record<string, {
8 requireAuth: boolean;
9 roles?: string[];
10 permissions?: string[];
11}> = {
12 '/admin': { requireAuth: true, roles: ['admin'] },
13 '/moderator': { requireAuth: true, roles: ['admin', 'moderator'] },
14 '/dashboard': { requireAuth: true },
15 '/profile': { requireAuth: true },
16 '/settings': { requireAuth: true },
17 '/': { requireAuth: false },
18 '/about': { requireAuth: false },
19 '/auth/login': { requireAuth: false },
20 '/auth/register': { requireAuth: false },
21};
22
23// API access configuration
24const apiAccessConfig: Record<string, {
25 roles?: string[];
26 permissions?: string[];
27}> = {
28 '/api/admin': { roles: ['admin'] },
29 '/api/moderator': { roles: ['admin', 'moderator'] },
30 '/api/users': { permissions: ['user:read'] },
31 '/api/users/create': { permissions: ['user:create'] },
32};
33
34// Helper functions for path matching (omitted for brevity)
35//
36export async function middleware(request: NextRequest) {
37 // Get the session token
38 const token = await getToken({ req: request });
39 const { pathname } = request.nextUrl;
40
41 // Check if this is an API request
42 const isApiRequest = pathname.startsWith('/api/');
43
44 // Skip authentication routes
45 if (pathname.startsWith('/api/auth/')) {
46 return NextResponse.next();
47 }
48
49 // API request handling
50 if (isApiRequest) {
51 // If it is API and the user is not logged in (for protected endpoints)
52 if (!token && !pathname.startsWith('/api/public/')) {
53 return NextResponse.json(
54 { error: 'Unauthorized - Authentication required' },
55 { status: 401 }
56 );
57 }
58
59 // Match API configuration
60 const apiMatch = matchApiConfig(pathname);
61 if (apiMatch && token) {
62 const [configPath, config] = apiMatch;
63
64 // Check role and permissions
65 if (!hasRequiredRoles(token, config.roles) ||
66 !hasRequiredPermissions(token, config.permissions)) {
67 return NextResponse.json(
68 { error: 'Forbidden - Insufficient permissions' },
69 { status: 403 }
70 );
71 }
72 }
73 }
74 // Page handling
75 else {
76 // Match page configuration
77 const pageMatch = matchPageConfig(pathname);
78 if (pageMatch) {
79 const [configPath, config] = pageMatch;
80
81 // Authentication pages - redirect if the user is logged in
82 if (
83 !config.requireAuth &&
84 (pathname === '/auth/login' || pathname === '/auth/register') &&
85 token
86 ) {
87 return NextResponse.redirect(new URL('/dashboard', request.url));
88 }
89
90 // If the page requires authentication and the user is not logged in
91 if (config.requireAuth && !token) {
92 const url = new URL('/auth/login', request.url);
93 url.searchParams.set('callbackUrl', pathname);
94 return NextResponse.redirect(url);
95 }
96
97 // If the user is logged in, check role and permissions
98 if (token && config.requireAuth) {
99 if (!hasRequiredRoles(token, config.roles) ||
100 !hasRequiredPermissions(token, config.permissions)) {
101 return NextResponse.redirect(new URL('/auth/unauthorized', request.url));
102 }
103 }
104 }
105 }
106
107 // All conditions met
108 return NextResponse.next();
109}
110
111// Helper functions for checking roles and permissions
112function hasRequiredRoles(token: any, requiredRoless?: string[]): boolean {
113 if (!requiredRoless || requiredRoless.length === 0) return true;
114
115 const userRoles = token.role as string;
116 return requiredRoless.includes(userRoles);
117}
118
119function hasRequiredPermissions(token: any, requiredPermissions?: string[]): boolean {
120 if (!requiredPermissions || requiredPermissions.length === 0) return true;
121
122 const userPermissions = token.permissions as string[] || [];
123 return requiredPermissions.every(permission => userPermissions.includes(permission));
124}
125
126// Functions matching path configuration (implementation omitted for brevity)
127function matchPageConfig(pathname: string) { /* ... */ }
128function matchApiConfig(pathname: string) { /* ... */ }
129
130export const config = {
131 matcher: [
132 '/((?!_next/static|_next/image|images|favicon.ico).*)',
133 ],
134};Often we need middleware that handles specific use cases. Let's look at some examples:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Simple in-memory request limit implementation
6// In production, you should use Redis or a similar solution
7const REQUESTS_PER_MINUTE = 60;
8const WINDOW_SIZE_MS = 60 * 1000; // 1 minute
9
10interface RateLimitState {
11 requests: number;
12 timestamp: number;
13}
14
15const ipRequestCounts = new Map<string, RateLimitState>();
16
17export function middleware(request: NextRequest) {
18 // Get the user's IP address
19 const ip = request.ip || 'unknown';
20
21 // Check if it is API
22 if (!request.nextUrl.pathname.startsWith('/api/')) {
23 return NextResponse.next();
24 }
25
26 // Get the current limit state for this IP
27 const now = Date.now();
28 const state = ipRequestCounts.get(ip) || { requests: 0, timestamp: now };
29
30 // If the time window has passed, reset the counter
31 if (now - state.timestamp > WINDOW_SIZE_MS) {
32 state.requests = 0;
33 state.timestamp = now;
34 }
35
36 // Increment the request counter
37 state.requests++;
38 ipRequestCounts.set(ip, state);
39
40 // If the limit is exceeded, return error 429 (Too Many Requests)
41 if (state.requests > REQUESTS_PER_MINUTE) {
42 const response = NextResponse.json(
43 { error: 'Rate limit exceeded. Please try again later.' },
44 { status: 429 }
45 );
46
47 // Set headers informing about limits
48 response.headers.set('X-RateLimit-Limit', REQUESTS_PER_MINUTE.toString());
49 response.headers.set('X-RateLimit-Remaining', '0');
50 response.headers.set('Retry-After',
51 Math.ceil((state.timestamp + WINDOW_SIZE_MS - now) / 1000).toString()
52 );
53
54 return response;
55 }
56
57 // Within the limit - continue
58 const response = NextResponse.next();
59
60 // Set headers informing about limits
61 response.headers.set('X-RateLimit-Limit', REQUESTS_PER_MINUTE.toString());
62 response.headers.set(
63 'X-RateLimit-Remaining',
64 Math.max(0, REQUESTS_PER_MINUTE - state.requests).toString()
65 );
66
67 return response;
68}
69
70export const config = {
71 matcher: ['/api/:path*'],
72};1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// List of allowed country codes
6const ALLOWED_COUNTRIES = ['US', 'CA', 'GB', 'DE', 'FR', 'PL'];
7
8// List of pages subject to geographic restrictions
9const GEO_RESTRICTED_PATHS = [
10 '/premium-content',
11 '/members-only',
12];
13
14export function middleware(request: NextRequest) {
15 // Check if the path is subject to geographic restrictions
16 const isGeoRestrictedPath = GEO_RESTRICTED_PATHS.some(path =>
17 request.nextUrl.pathname === path ||
18 request.nextUrl.pathname.startsWith(`${path}/`)
19 );
20
21 if (isGeoRestrictedPath) {
22 // Get the user's country information from headers (available on the Vercel platform)
23 const country = request.geo?.country || 'Unknown';
24
25 // If the user's country is not on the allowed list
26 if (!ALLOWED_COUNTRIES.includes(country)) {
27 return NextResponse.redirect(new URL('/geo-restricted', request.url));
28 }
29 }
30
31 return NextResponse.next();
32}
33
34export const config = {
35 matcher: [
36 '/premium-content/:path*',
37 '/members-only/:path*',
38 ],
39};1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 // Get the original response
7 const response = NextResponse.next();
8
9 // Add security headers
10
11 // Protect against XSS attacks
12 response.headers.set('X-XSS-Protection', '1; mode=block');
13
14 // Prevent MIME type "sniffing"
15 response.headers.set('X-Content-Type-Options', 'nosniff');
16
17 // Control how the page can be embedded in an iframe
18 response.headers.set('X-Frame-Options', 'SAMEORIGIN');
19
20 // Content Security Policy - more advanced protection against XSS
21 response.headers.set(
22 'Content-Security-Policy',
23 "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https://api.example.com;"
24 );
25
26 // HTTP Strict Transport Security - forces HTTPS use
27 response.headers.set(
28 'Strict-Transport-Security',
29 'max-age=31536000; includeSubDomains; preload'
30 );
31
32 // Permissions Policy - declare which browser features are allowed
33 response.headers.set(
34 'Permissions-Policy',
35 'camera=(), microphone=(), geolocation=(self), interest-cohort=()'
36 );
37
38 return response;
39}
40
41export const config = {
42 matcher: [
43 '/((?!_next/static|_next/image|images|favicon.ico).*)',
44 ],
45};1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// A/B experiment configuration
6const AB_TESTS = {
7 'new-landing-page': {
8 variants: ['control', 'variant-a', 'variant-b'],
9 weights: [0.34, 0.33, 0.33], // Traffic split
10 paths: ['/'], // Paths covered by the test
11 cookieName: 'ab-new-landing',
12 },
13 'checkout-flow': {
14 variants: ['original', 'simplified'],
15 weights: [0.5, 0.5],
16 paths: ['/checkout', '/cart'],
17 cookieName: 'ab-checkout',
18 },
19};
20
21export function middleware(request: NextRequest) {
22 const url = request.nextUrl;
23 const response = NextResponse.next();
24
25 // Iterate through the defined tests
26 for (const [testName, test] of Object.entries(AB_TESTS)) {
27 // Check if the current path is covered by the test
28 const isTestPath = test.paths.some(path =>
29 url.pathname === path || url.pathname.startsWith(`${path}/`)
30 );
31
32 if (isTestPath) {
33 // Check if the user already has a test variant assigned
34 const existingVariant = request.cookies.get(test.cookieName)?.value;
35 let variant = existingVariant;
36
37 // If the user has no assigned variant, assign a new one
38 if (!variant) {
39 variant = assignVariant(test.variants, test.weights);
40
41 // Set a cookie with the assigned variant
42 response.cookies.set(test.cookieName, variant, {
43 maxAge: 60 * 60 * 24 * 30, // 30 days
44 path: '/',
45 });
46 }
47
48 // Add a header with test variant information
49 response.headers.set(`X-AB-Test-${testName}`, variant);
50
51 // You can also set a rewrite to a different page for different variants
52 if (testName === 'new-landing-page' && url.pathname === '/') {
53 if (variant === 'variant-a') {
54 // Internal redirect (invisible to the user)
55 return NextResponse.rewrite(new URL('/landing-a', request.url));
56 } else if (variant === 'variant-b') {
57 return NextResponse.rewrite(new URL('/landing-b', request.url));
58 }
59 }
60 }
61 }
62
63 return response;
64}
65
66// Function that assigns a variant based on weights
67function assignVariant(variants: string[], weights: number[]): string {
68 const randomNum = Math.random();
69 let cumulativeWeight = 0;
70
71 for (let i = 0; i < variants.length; i++) {
72 cumulativeWeight += weights[i];
73 if (randomNum < cumulativeWeight) {
74 return variants[i];
75 }
76 }
77
78 // Fall back to the first variant
79 return variants[0];
80}
81
82export const config = {
83 matcher: [
84 '/', '/checkout', '/cart',
85 '/landing-a', '/landing-b',
86 ],
87};Tests are very important to verify that middleware works correctly, especially since it handles key application security aspects. Here is a simple example of unit tests:
1// __tests__/middleware.test.ts
2import { NextRequest } from 'next/server';
3import { middleware } from '../middleware';
4import { getToken } from 'next-auth/jwt';
5
6// We mock getToken from next-auth/jwt
7jest.mock('next-auth/jwt', () => ({
8 getToken: jest.fn(),
9}));
10
11describe('Auth Middleware', () => {
12 // Setup and reset before each test
13 beforeEach(() => {
14 jest.clearAllMocks();
15 });
16
17 it('should redirect unauthenticated user from protected route to login page', async () => {
18 // Mock getToken returning null (unauthenticated user)
19 (getToken as jest.Mock).mockResolvedValueOnce(null);
20
21 // Create a test request to a protected path
22 const req = new NextRequest(new URL('http://localhost:3000/dashboard'));
23
24 // Run the middleware
25 const response = await middleware(req);
26
27 // Check that it redirects to the login page
28 expect(response).not.toBeNull();
29 expect(response.status).toBe(307); // Kod przekierowania
30 expect(response.headers.get('location')).toBe('http://localhost:3000/auth/login?callbackUrl=%2Fdashboard');
31 });
32
33 it('should allow authenticated user to access protected route', async () => {
34 // Mock getToken returning a token (logged-in user)
35 (getToken as jest.Mock).mockResolvedValueOnce({
36 name: 'Test User',
37 email: 'test@example.com',
38 role: 'user',
39 permissions: ['content:read']
40 });
41
42 // Create a test request to a protected path
43 const req = new NextRequest(new URL('http://localhost:3000/dashboard'));
44
45 // Run the middleware
46 const response = await middleware(req);
47
48 // Check that it allows access
49 expect(response).not.toBeNull();
50 expect(response.status).toBe(200);
51 });
52
53 it('should redirect authenticated user from login page to dashboard', async () => {
54 // Mock getToken returning a token (logged-in user)
55 (getToken as jest.Mock).mockResolvedValueOnce({
56 name: 'Test User',
57 email: 'test@example.com',
58 });
59
60 // Create a test request to the login page
61 const req = new NextRequest(new URL('http://localhost:3000/auth/login'));
62
63 // Run the middleware
64 const response = await middleware(req);
65
66 // Check that it redirects to the dashboard
67 expect(response).not.toBeNull();
68 expect(response.status).toBe(307);
69 expect(response.headers.get('location')).toBe('http://localhost:3000/dashboard');
70 });
71
72 it('should redirect user without admin role from admin page', async () => {
73 // Mock getToken returning a token with user role
74 (getToken as jest.Mock).mockResolvedValueOnce({
75 name: 'Test User',
76 email: 'test@example.com',
77 role: 'user',
78 permissions: ['content:read']
79 });
80
81 // Create a test request to the admin page
82 const req = new NextRequest(new URL('http://localhost:3000/admin/dashboard'));
83
84 // Run the middleware
85 const response = await middleware(req);
86
87 // Check that it redirects to the no-permissions page
88 expect(response).not.toBeNull();
89 expect(response.status).toBe(307);
90 expect(response.headers.get('location')).toBe('http://localhost:3000/auth/unauthorized');
91 });
92});Middleware is executed on every request, so it's important that it's as efficient as possible:
Debugging middleware can be difficult because it runs "behind the scenes":
console.log() to trace middleware activity.1// Example of adding debug headers
2const response = NextResponse.next();
3response.headers.set('X-Debug-Path', request.nextUrl.pathname);
4response.headers.set('X-Debug-Auth', token ? 'Authenticated' : 'Unauthenticated');
5return response;For complex applications, it's worth considering middleware modularization:
1// middleware/auth.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { getToken } from 'next-auth/jwt';
4
5export async function authMiddleware(req: NextRequest) {
6 // Authentication middleware implementation
7 //
8}
9
10// middleware/security.ts
11import { NextRequest, NextResponse } from 'next/server';
12
13export function securityHeadersMiddleware(req: NextRequest) {
14 // Security headers middleware implementation
15 //
16}
17
18// middleware.ts
19import { NextRequest, NextResponse } from 'next/server';
20import { authMiddleware } from './middleware/auth';
21import { securityHeadersMiddleware } from './middleware/security';
22
23export async function middleware(req: NextRequest) {
24 // Check if the path requires authentication
25 if (req.nextUrl.pathname.startsWith('/dashboard') ||
26 req.nextUrl.pathname.startsWith('/admin')) {
27 return await authMiddleware(req);
28 }
29
30 // Add security headers to all responses
31 return securityHeadersMiddleware(req);
32}
33
34export const config = { /* ... */ };Middleware in Next.js offers a powerful mechanism for implementing route protection and many other server-level features. In this module we learned:
A good middleware implementation is a key element of Next.js application security and performance. Thanks to the techniques presented in this module, you can create advanced, multi-layered protections for your applications.
In the next module, we will move on to implementing a Context Provider for authentication, which will enable easy access to authentication data throughout the application.