Next.js course Β· Module 2: Routing and Layouts

Middleware and Request Interception

17 min read
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:

  1. Authorization for protected zones
  2. Internationalization and language detection
  3. 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:

  1. Exact matching: '/about'
  2. Matching with parameters: '/blog/:path'
  3. Matching with catch-all parameters: '/blog/:path*'
  4. Several paths at once: an array ['/dashboard/:path*', '/admin/:path*'] or a group in parentheses '/(dashboard|admin)/:path*'
  5. 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:

  1. Implement advanced authorization systems - verify JWT tokens, redirect unauthenticated users, control access to protected resources.

  2. Add internationalization features - detect the user's preferred language, redirect to appropriate language versions.

  3. Personalize user experience - adjust content based on location, preferences, and user history.

  4. Optimize performance - control caching headers, modify responses, implement rate limiting.

  5. Integrate with external systems - connect with analytics, authentication, and other services.

  6. Conduct A/B tests - assign users to test groups and personalize the experience.

  7. 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. 1. Metadata in App Router is defined by:

  2. 2. Private Folders in App Router are marked with the prefix:

  3. 3. By default, components in App Router without the 'use client' directive:

  4. 4. To create a Client Component, you need to add:

  5. 5. What is prefetching in Next.js?

  6. 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. 7. How does router.replace() differ from router.push() from the useRouter hook?

  8. 8. What prefix must an environment variable have to be accessible on the client side in Next.js?

  9. 9. Parallel Routes in App Router use the naming convention:

  10. 10. What happens when you call the notFound() function inside a Server Component in Next.js 16?

  11. 11. Why must the error.tsx file in Next.js 16 be a Client Component (with the 'use client' directive)?

  12. 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

Useful articles