Next.js course Β· Module 2: Routing and Layouts
Middleware and Request Interception
In this lesson15
At the heart of the Quantum Metropolis lies an advanced access control and redirect system known as the Quantum Gateway. This system monitors every access attempt to various city zones, verifies permissions, redirects to appropriate points, and modifies travel routes in real time. It's an extremely important infrastructure element that ensures both security and traffic optimization throughout the city.
In the world of Next.js 16, Proxy (called Middleware up to version 15) plays a similar role - a powerful mechanism that allows intercepting and modifying HTTP requests before they are handled by a page or API, enabling the implementation of features like authorization, redirects, or response modifications.
What is Proxy (formerly Middleware) in Next.js 16?
Proxy is a function that is executed before a request is handled by a page or API route. It operates between the client request and server response, providing the ability to:
- Execute code before loading the page
- Modify the request object
- Modify the response object
- Redirect the user
- Reject requests
- Add HTTP headers
- Modify cookies
Basic Middleware Structure
In Next.js 16, you put this code in a proxy.ts (or proxy.js) file in the project root directory (next to app or pages, or inside src if your project uses it). Up to version 15 the file was called middleware.ts and the exported function middleware: Next.js 16 renamed both, but the logic stayed the same. The old file still works, but it is deprecated, and the proxy always runs in the Node.js runtime.
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// The proxy function is executed before every request
6export function proxy(request: NextRequest) {
7 // Here we can implement our logic
8 console.log('Proxy executed for:', request.nextUrl.pathname);
9
10 // We can modify the response object
11 return NextResponse.next();
12}
13
14// Optionally, we can limit which routes the proxy runs for
15export const config = {
16 matcher: '/api/:path*',
17};Available Middleware Operations
1. Redirects and Rewrites
Just as in the Quantum Metropolis, where security gates can automatically redirect unauthorized visitors to checkpoints, middleware in Next.js allows redirecting requests:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Check if the user is trying to access the admin zone
7 if (request.nextUrl.pathname.startsWith('/admin')) {
8 // Check if the user is logged in (e.g., by checking a cookie)
9 const isLoggedIn = request.cookies.has('auth-token');
10
11 if (!isLoggedIn) {
12 // Redirect unauthenticated users to the login page
13 const loginUrl = new URL('/login', request.url);
14 // Add the original URL as a parameter so we can return after login
15 loginUrl.searchParams.set('from', request.nextUrl.pathname);
16 return NextResponse.redirect(loginUrl);
17 }
18 }
19
20 // Otherwise, continue with normal request processing
21 return NextResponse.next();
22}2. Modifying Request or Response Headers
We can also modify headers, just as the Quantum Gateway can add special identifiers to transport tickets:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Get the original response
7 const response = NextResponse.next();
8
9 // Add custom headers to the response
10 response.headers.set('x-quantum-security', 'enabled');
11 response.headers.set('x-quantum-gateway-version', '15.0.1');
12
13 // Add Content Security Policy (CSP)
14 response.headers.set(
15 'Content-Security-Policy',
16 "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' quantum-cdn.example.com;"
17 );
18
19 return response;
20}3. Manipulating Cookies
Middleware can also manipulate cookies, which is useful for session management:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 const response = NextResponse.next();
7
8 // Read cookie value
9 const theme = request.cookies.get('theme')?.value;
10
11 // Set a new cookie
12 response.cookies.set('last-visit', new Date().toISOString());
13
14 // Change existing cookie value
15 if (theme === 'dark') {
16 response.cookies.set('theme-version', 'dark-v2');
17 }
18
19 // Delete cookie
20 if (request.nextUrl.pathname === '/logout') {
21 response.cookies.delete('auth-token');
22 }
23
24 return response;
25}4. Rewrite - URL Substitution Without Changing the Visible Address
Rewrite allows internally substituting the URL without changing the address visible to the user:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Example: internally redirect all requests from /docs to /documentation
7 // The address in the browser bar remains /docs
8 if (request.nextUrl.pathname.startsWith('/docs')) {
9 const url = request.nextUrl.clone();
10 url.pathname = url.pathname.replace(/^\/docs/, '/documentation');
11 return NextResponse.rewrite(url);
12 }
13
14 return NextResponse.next();
15}Implementing Middleware for the Quantum Voyages Application
Let's prepare comprehensive middleware for our Quantum Voyages application that will handle:
- Authorization for protected zones
- Internationalization and language detection
- Tracking and analytics
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { verifyAuthToken } from './lib/auth';
5
6// List of supported languages
7const SUPPORTED_LOCALES = ['pl', 'en', 'de', 'fr'];
8// Default language
9const DEFAULT_LOCALE = 'pl';
10// Paths requiring authorization
11const PROTECTED_PATHS = ['/dashboard', '/account', '/bookings'];
12// Admin paths requiring admin permissions
13const ADMIN_PATHS = ['/admin'];
14
15export async function proxy(request: NextRequest) {
16 const { pathname } = request.nextUrl;
17
18 // 1. Handle localization and internationalization
19 const pathnameHasLocale = SUPPORTED_LOCALES.some(
20 locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
21 );
22
23 // If the path doesn't contain a language code yet, redirect and add the code
24 if (!pathnameHasLocale) {
25 // Attempt to detect the user's preferred language
26 const preferredLocale = getPreferredLocale(request);
27 const url = new URL(request.url);
28 url.pathname = `/${preferredLocale}${pathname}`;
29 return NextResponse.redirect(url);
30 }
31
32 // Extract the language code from the path
33 const locale = pathname.split('/')[1];
34
35 // 2. Authorization for protected zones
36 // Check if the path is protected
37 if (isProtectedPath(pathname)) {
38 const authToken = request.cookies.get('auth-token')?.value;
39
40 // Verify the authorization token
41 const authResult = await verifyAuthToken(authToken);
42
43 // If the user is not logged in or the token is invalid
44 if (!authResult.isValid) {
45 const loginUrl = new URL(`/${locale}/login`, request.url);
46 loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
47 return NextResponse.redirect(loginUrl);
48 }
49
50 // Check admin permissions for administrative paths
51 if (isAdminPath(pathname) && !authResult.isAdmin) {
52 const accessDeniedUrl = new URL(`/${locale}/access-denied`, request.url);
53 return NextResponse.redirect(accessDeniedUrl);
54 }
55 }
56
57 // 3. Add analytics and security headers
58 const response = NextResponse.next();
59
60 // Add user info for analytics tools (no personal data)
61 const visitorId = request.cookies.get('visitor-id')?.value || generateVisitorId();
62 response.cookies.set('visitor-id', visitorId, {
63 httpOnly: true,
64 sameSite: 'strict',
65 maxAge: 60 * 60 * 24 * 365 // 1 rok
66 });
67
68 // Security headers
69 response.headers.set('X-Frame-Options', 'DENY');
70 response.headers.set('X-Content-Type-Options', 'nosniff');
71 response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
72
73 return response;
74}
75
76// Helper functions
77
78// Detecting the user's preferred language
79function getPreferredLocale(request: NextRequest): string {
80 // Check previously saved preferred language in cookie
81 const savedLocale = request.cookies.get('preferred-locale')?.value;
82 if (savedLocale && SUPPORTED_LOCALES.includes(savedLocale)) {
83 return savedLocale;
84 }
85
86 // Check the Accept-Language header
87 const acceptLanguage = request.headers.get('accept-language');
88 if (acceptLanguage) {
89 // Parse Accept-Language header and find the best match
90 const userLocales = acceptLanguage.split(',')
91 .map(item => {
92 const [locale, priority = 'q=1.0'] = item.trim().split(';');
93 const q = parseFloat(priority.replace('q=', '')) || 1.0;
94 return { locale: locale.split('-')[0], q };
95 })
96 .sort((a, b) => b.q - a.q);
97
98 // Find the first supported language
99 for (const { locale } of userLocales) {
100 if (SUPPORTED_LOCALES.includes(locale)) {
101 return locale;
102 }
103 }
104 }
105
106 // Default language if preferences cannot be determined
107 return DEFAULT_LOCALE;
108}
109
110// Check if the path is protected
111function isProtectedPath(pathname: string): boolean {
112 return PROTECTED_PATHS.some(path => {
113 // Remove the language code from the path before comparison
114 const pathWithoutLocale = pathname.split('/').slice(2).join('/');
115 return `/${pathWithoutLocale}`.startsWith(path);
116 });
117}
118
119// Check if the path is administrative
120function isAdminPath(pathname: string): boolean {
121 return ADMIN_PATHS.some(path => {
122 // Remove the language code from the path before comparison
123 const pathWithoutLocale = pathname.split('/').slice(2).join('/');
124 return `/${pathWithoutLocale}`.startsWith(path);
125 });
126}
127
128// Generate a unique ID for the visitor
129function generateVisitorId(): string {
130 return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
131}
132
133// Determine which paths the proxy will run for
134export const config = {
135 matcher: [
136 // Match all paths
137 '/((?!api|_next/static|_next/image|favicon.ico).*)',
138 ],
139};Selective Middleware Execution with Matcher
Just as in the Quantum Metropolis, where not every movement requires a full security check, in Next.js 16 we can specify which paths the proxy will run for using the matcher configuration:
1// proxy.ts
2export const config = {
3 matcher: [
4 // Paths for which the proxy will run
5 '/dashboard/:path*',
6 '/api/:path*',
7 '/((?!_next/static|_next/image|favicon.ico).*)',
8 ],
9};Various matcher patterns are available:
- Exact matching:
'/about' - Matching with parameters:
'/blog/:path' - Matching with catch-all parameters:
'/blog/:path*' - Several paths at once: an array
['/dashboard/:path*', '/admin/:path*']or a group in parentheses'/(dashboard|admin)/:path*' - Negation matching:
/((?!api|_next/static|_next/image|favicon.ico).*)
Implementing JWT Authorization with Middleware
In the Quantum Metropolis, resident identifiers are verified using advanced cryptographic technology. Let's implement a similar JWT authorization system in our application:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { jwtVerify } from 'jose';
5
6interface UserPayload {
7 id: string;
8 email: string;
9 role: string;
10 exp: number;
11}
12
13export async function proxy(request: NextRequest) {
14 // Check if the path requires authorization
15 if (request.nextUrl.pathname.startsWith('/dashboard') ||
16 request.nextUrl.pathname.startsWith('/api/protected')) {
17
18 // Get JWT token from cookie or Authorization header
19 const authToken = request.cookies.get('auth-token')?.value ||
20 request.headers.get('Authorization')?.split(' ')[1];
21
22 if (!authToken) {
23 // No token - redirect to login
24 return redirectToLogin(request);
25 }
26
27 try {
28 // Verify JWT token
29 const secretKey = new TextEncoder().encode(process.env.JWT_SECRET!);
30 const { payload } = await jwtVerify<UserPayload>(authToken, secretKey);
31
32 // Check if the token has expired
33 const currentTime = Math.floor(Date.now() / 1000);
34 if (payload.exp < currentTime) {
35 // Token expired - redirect to login
36 return redirectToLogin(request);
37 }
38
39 // Check admin permissions for administrative paths
40 if (request.nextUrl.pathname.startsWith('/admin') && payload.role !== 'admin') {
41 return NextResponse.json(
42 { message: 'Insufficient permissions' },
43 { status: 403 }
44 );
45 }
46
47 // Add user info to headers for subsequent handlers
48 const response = NextResponse.next();
49 response.headers.set('x-user-id', payload.id);
50 response.headers.set('x-user-role', payload.role);
51
52 return response;
53 } catch (error) {
54 // Token verification error - redirect to login
55 return redirectToLogin(request);
56 }
57 }
58
59 return NextResponse.next();
60}
61
62// Function that redirects to login
63function redirectToLogin(request: NextRequest) {
64 const loginUrl = new URL('/login', request.url);
65 loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
66 return NextResponse.redirect(loginUrl);
67}
68
69export const config = {
70 matcher: [
71 '/dashboard/:path*',
72 '/admin/:path*',
73 '/api/protected/:path*',
74 ],
75};Implementing Theme Switching with Middleware
Quantum City offers its residents the ability to customize the appearance of city interfaces. Let's implement a similar system for switching between light and dark themes:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Get preferred theme
7 const theme = request.cookies.get('theme')?.value || 'light';
8
9 // Create response
10 const response = NextResponse.next();
11
12 // Set theme preference variable as a cookie
13 response.cookies.set('theme', theme, {
14 maxAge: 60 * 60 * 24 * 365, // valid for one year
15 path: '/',
16 });
17
18 // Add a custom header that can be read by the client
19 response.headers.set('x-theme', theme);
20
21 return response;
22}
23
24export const config = {
25 matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
26};Handling Multiple Domains and Subdomains with Middleware
In the Quantum Metropolis, different districts can have their own access systems. Similarly in Next.js, we can handle different domains and subdomains:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 const { pathname, hostname } = request.nextUrl;
7
8 // Handle different subdomains
9 if (hostname === 'admin.quantumvoyages.com') {
10 // Redirect all requests to the admin panel
11 const url = new URL('/admin' + pathname, request.url);
12 return NextResponse.rewrite(url);
13 }
14
15 if (hostname === 'api.quantumvoyages.com') {
16 // Redirect requests to the API
17 const url = new URL('/api' + pathname, request.url);
18 return NextResponse.rewrite(url);
19 }
20
21 if (hostname.startsWith('user-')) {
22 // Handle user subdomains (e.g., user-john.quantumvoyages.com)
23 const username = hostname.replace('user-', '').split('.')[0];
24 const url = new URL(`/profile/${username}${pathname}`, request.url);
25 return NextResponse.rewrite(url);
26 }
27
28 return NextResponse.next();
29}
30
31export const config = {
32 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
33};Caching and Response Header Modification
In the Quantum Metropolis, the Gateway system optimizes data flow through its systems. Similarly, we can optimize our application:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 const response = NextResponse.next();
7
8 // Set caching headers for static assets
9 if (request.nextUrl.pathname.match(/\.(jpg|jpeg|png|webp|svg|gif|ico)$/)) {
10 response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
11 }
12
13 // Set caching headers for HTML pages
14 else if (!request.nextUrl.pathname.startsWith('/api/')) {
15 response.headers.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
16 }
17
18 // Add security headers
19 response.headers.set('Strict-Transport-Security', 'max-age=63072000');
20 response.headers.set('X-Content-Type-Options', 'nosniff');
21 response.headers.set('X-Frame-Options', 'DENY');
22 response.headers.set('X-XSS-Protection', '1; mode=block');
23
24 return response;
25}
26
27export const config = {
28 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
29};Implementing Geolocation and Content Personalization
The Quantum Metropolis adjusts available services based on resident locations. Let's implement a similar system in our application, using middleware to determine user location:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Table of regions and their available destinations
6const REGION_DESTINATIONS = {
7 'EU': ['mars', 'europa', 'titan'],
8 'NA': ['mars', 'luna', 'titan', 'ganymede'],
9 'ASIA': ['mars', 'venus-orbiter', 'luna'],
10 'default': ['mars'] // default destinations available globally
11};
12
13export function proxy(request: NextRequest) {
14 // Get user's region (in reality, you could use an external geolocation API)
15 const countryCode = request.headers.get('x-country') || 'default';
16 const region = getRegionFromCountry(countryCode);
17
18 // Create response
19 const response = NextResponse.next();
20
21 // Add region info and available destinations as headers
22 response.headers.set('x-user-region', region);
23 response.headers.set('x-available-destinations', JSON.stringify(REGION_DESTINATIONS[region] || REGION_DESTINATIONS.default));
24
25 // Set region cookie (will be available in the browser)
26 response.cookies.set('user-region', region, {
27 maxAge: 60 * 60 * 24 * 30, // 30 dni
28 path: '/',
29 });
30
31 return response;
32}
33
34// Function mapping country codes to regions
35function getRegionFromCountry(countryCode: string): keyof typeof REGION_DESTINATIONS {
36 const EU_COUNTRIES = ['DE', 'FR', 'IT', 'ES', 'PL', 'NL', 'BE', 'SE', 'AT', 'DK'];
37 const NA_COUNTRIES = ['US', 'CA', 'MX'];
38 const ASIA_COUNTRIES = ['JP', 'CN', 'KR', 'IN', 'SG', 'TH', 'MY', 'PH', 'VN'];
39
40 if (EU_COUNTRIES.includes(countryCode)) return 'EU';
41 if (NA_COUNTRIES.includes(countryCode)) return 'NA';
42 if (ASIA_COUNTRIES.includes(countryCode)) return 'ASIA';
43
44 return 'default';
45}
46
47export const config = {
48 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
49};Request Rate Limiting
In the Quantum Metropolis, the Gateway system controls the number of requests to critical resources to prevent overload. Let's implement a similar rate limiting mechanism:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Simple memory-based rate limiting system (in production, better to use Redis or a similar service)
6const RATE_LIMIT_DURATION = 60 * 1000; // 1 minuta
7const MAX_REQUESTS_PER_MINUTE = 60; // Maximum number of requests per minute
8
9// Storing request counters for different IPs (in a real application, better to use Redis)
10const ipRequestCounts = new Map<string, { count: number, resetAt: number }>();
11
12export function proxy(request: NextRequest) {
13 // We only limit API requests
14 if (request.nextUrl.pathname.startsWith('/api/')) {
15 // NextRequest no longer has an ip field (removed in Next.js 15) - the x-forwarded-for header carries the client address
16 const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown';
17 const now = Date.now();
18
19 // Get or create a counter for this IP
20 if (!ipRequestCounts.has(ip) || ipRequestCounts.get(ip)!.resetAt < now) {
21 // Reset the counter if time has elapsed
22 ipRequestCounts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_DURATION });
23 } else {
24 // Increment counter
25 const record = ipRequestCounts.get(ip)!;
26 record.count++;
27
28 // Check if the limit has been exceeded
29 if (record.count > MAX_REQUESTS_PER_MINUTE) {
30 // Calculate the time after which requests can be sent again
31 const resetTime = Math.ceil((record.resetAt - now) / 1000);
32
33 // Return a response with 429 Too Many Requests code
34 return new NextResponse('Rate limit exceeded', {
35 status: 429,
36 headers: {
37 'Retry-After': `${resetTime}`,
38 'X-RateLimit-Limit': `${MAX_REQUESTS_PER_MINUTE}`,
39 'X-RateLimit-Remaining': '0',
40 'X-RateLimit-Reset': `${resetTime}`,
41 },
42 });
43 }
44 }
45 }
46
47 return NextResponse.next();
48}
49
50export const config = {
51 matcher: '/api/:path*',
52};A/B Testing with Middleware
In the Quantum Metropolis, some new features are tested on selected groups of residents. Let's implement a similar A/B testing system:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// A/B test definitions
6const AB_TESTS = {
7 'new-landing-page': {
8 variants: ['control', 'variant-a', 'variant-b'],
9 weights: [0.5, 0.25, 0.25], // 50% of users will see control, 25% variant-a, 25% variant-b
10 },
11 'checkout-flow': {
12 variants: ['standard', 'simplified'],
13 weights: [0.5, 0.5], // 50% of users will see each variant
14 },
15};
16
17export function proxy(request: NextRequest) {
18 const response = NextResponse.next();
19
20 // Assign the user to test groups
21 Object.entries(AB_TESTS).forEach(([testName, test]) => {
22 // Get existing variant from cookie or assign a new one
23 let variant = request.cookies.get(`ab-${testName}`)?.value;
24
25 if (!variant || !test.variants.includes(variant)) {
26 // Assign variant based on weights
27 variant = assignVariant(test.variants, test.weights);
28
29 // Save the variant in a cookie
30 response.cookies.set(`ab-${testName}`, variant, {
31 maxAge: 60 * 60 * 24 * 30, // 30 dni
32 path: '/',
33 });
34 }
35
36 // Add variant info as a header for the client
37 response.headers.set(`x-ab-${testName}`, variant);
38 });
39
40 return response;
41}
42
43// Function assigning a variant based on weights
44function assignVariant(variants: string[], weights: number[]): string {
45 const random = Math.random();
46 let cumulativeWeight = 0;
47
48 for (let i = 0; i < variants.length; i++) {
49 cumulativeWeight += weights[i];
50 if (random < cumulativeWeight) {
51 return variants[i];
52 }
53 }
54
55 // Default to the first variant
56 return variants[0];
57}
58
59export const config = {
60 matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
61};Monitoring and Debugging Middleware
In production applications, it's important to monitor middleware behavior. We can implement a simple logging system:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 const startTime = Date.now();
7
8 // Create a request identifier
9 const requestId = generateRequestId();
10
11 // Create response
12 const response = NextResponse.next();
13
14 // Add request identifier to headers
15 response.headers.set('x-request-id', requestId);
16
17 // Time spent in the proxy itself (the page renders only after it, so this is not the time of the whole request)
18 const duration = Date.now() - startTime;
19 response.headers.set('x-response-time', `${duration}ms`);
20
21 // In production, you could send this data to a monitoring system
22 // logRequest(requestId, request.method, request.url, response.status, duration);
23
24 return response;
25}
26
27// Generating a unique request identifier
28function generateRequestId(): string {
29 return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
30}
31
32export const config = {
33 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
34};Integration with External Systems
Middleware can also integrate with external systems, such as authentication or analytics systems:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export async function proxy(request: NextRequest) {
6 // Get session info from an external service
7 const sessionId = request.cookies.get('session-id')?.value;
8 let userInfo = null;
9
10 if (sessionId) {
11 try {
12 // In a real application, this could be an API call
13 userInfo = await fetchUserInfo(sessionId);
14 } catch (error) {
15 console.error('Error fetching user info:', error);
16 }
17 }
18
19 // Create response
20 const response = NextResponse.next();
21
22 // If we have user info, add it to headers
23 if (userInfo) {
24 response.headers.set('x-user-id', userInfo.id);
25 response.headers.set('x-user-plan', userInfo.plan);
26
27 // Send data to the analytics system
28 trackUserActivity(userInfo.id, request.url, request.method);
29 }
30
31 return response;
32}
33
34// Function fetching user info (simulation)
35async function fetchUserInfo(sessionId: string) {
36 // In a real application, this could be an API call
37 return new Promise(resolve => {
38 setTimeout(() => {
39 resolve({
40 id: 'user-123',
41 plan: 'premium',
42 name: 'John Doe'
43 });
44 }, 50);
45 });
46}
47
48// Function tracking user activity (simulation)
49function trackUserActivity(userId: string, url: string, method: string) {
50 // In a real application, this could send data to Google Analytics, Mixpanel, etc.
51 console.log(`User ${userId} accessed ${url} with method ${method}`);
52}
53
54export const config = {
55 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
56};Summary
Proxy in Next.js 16 (formerly Middleware) is a powerful tool that, like the Quantum Gateway in the Quantum Metropolis, enables controlling and modifying traffic in the application at the very beginning of the request-response cycle. Thanks to this, we can:
Implement advanced authorization systems - verify JWT tokens, redirect unauthenticated users, control access to protected resources.
Add internationalization features - detect the user's preferred language, redirect to appropriate language versions.
Personalize user experience - adjust content based on location, preferences, and user history.
Optimize performance - control caching headers, modify responses, implement rate limiting.
Integrate with external systems - connect with analytics, authentication, and other services.
Conduct A/B tests - assign users to test groups and personalize the experience.
Monitor and debug - track requests, measure performance, record metrics.
By leveraging middleware, we can significantly extend the capabilities of our Next.js 16 application, adding advanced features that are executed early in the request lifecycle, allowing for greater control and better performance.
In the next lesson you will learn prefetching and other ways to speed up navigation in Next.js 16.
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Metadata in App Router is defined by:
2. Private Folders in App Router are marked with the prefix:
3. By default, components in App Router without the 'use client' directive:
4. To create a Client Component, you need to add:
5. What is prefetching in Next.js?
6. In Next.js 16, what must the file in the project root that replaced middleware.ts and runs before a request is handled be called?
7. How does router.replace() differ from router.push() from the useRouter hook?
8. What prefix must an environment variable have to be accessible on the client side in Next.js?
9. Parallel Routes in App Router use the naming convention:
10. What happens when you call the notFound() function inside a Server Component in Next.js 16?
11. Why must the error.tsx file in Next.js 16 be a Client Component (with the 'use client' directive)?
12. What is the template.tsx file best suited for instead of layout.tsx?
Hands-on tasks in the game
- Code editor
Create the catch-all route app/docs/[...slug]/page.js: default-export an async DocsPage({ params }) component, read the array of segments with const { slug } = await params and display each URL segment in its own li element of a ul list.
- Vertical ordering
Arrange the order of handling an error with the error.js file
- Click in order
Arrange the correct file path for a catch-all route in Next.js
- Vertical ordering
Arrange the syntax of the root layout (app/layout.tsx) in App Router
- Vertical ordering
Arrange the static page generation order in App Router
- Vertical ordering
Arrange the root layout syntax with html and body tags
- Click in order
Arrange the correct path to a dynamic product page in App Router
- Vertical ordering
Arrange the Incremental Static Regeneration process order
- Click in order
Arrange the usePathname syntax in a component
- Vertical ordering
Arrange the steps of navigating with the Link component: from the link appearing on screen to the page update
- Vertical ordering
Arrange the metadata resolution order in Next.js
- Vertical ordering
Arrange the import and usage syntax of the useRouter hook
- Click in order
Arrange the generateMetadata syntax
- Vertical ordering
Arrange the steps of optimizing a font with next/font (from the build to displaying text)
- Vertical ordering
Arrange the usage syntax of the Script component from Next.js
- Vertical ordering
Arrange the Server Components rendering order