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 15, Middleware 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.
Middleware in Next.js 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:
In Next.js 15, middleware is defined in a
middleware.ts (or middleware.js) file in the project root directory (next to app or pages).1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// The middleware function is executed before every request
6export function middleware(request: NextRequest) {
7 // Here we can implement our logic
8 console.log('Middleware 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 middleware runs for
15export const config = {
16 matcher: '/api/:path*',
17};Just as in the Quantum Metropolis, where security gates can automatically redirect unauthorized visitors to checkpoints, middleware in Next.js allows redirecting requests:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(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}We can also modify headers, just as the Quantum Gateway can add special identifiers to transport tickets:
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 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}Middleware can also manipulate cookies, which is useful for session management:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(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}Rewrite allows internally substituting the URL without changing the address visible to the user:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(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}Let's prepare comprehensive middleware for our Quantum Voyages application that will handle:
1// middleware.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 middleware(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 middleware will run for
134export const config = {
135 matcher: [
136 // Match all paths
137 '/((?!api|_next/static|_next/image|favicon.ico).*)',
138 ],
139};Just as in the Quantum Metropolis, where not every movement requires a full security check, in Next.js 15 we can specify which paths the middleware will run for using the
matcher configuration:1// middleware.ts
2export const config = {
3 matcher: [
4 // Paths for which the middleware will run
5 '/dashboard/:path*',
6 '/api/:path*',
7 '/((?!_next/static|_next/image|favicon.ico).*)',
8 ],
9};Various matcher patterns are available:
'/about''/blog/:path''/blog/:path*'/dashboard/:path*|/admin/:path*/((?!api|_next/static|_next/image|favicon.ico).*)In the Quantum Metropolis, resident identifiers are verified using advanced cryptographic technology. Let's implement a similar JWT authorization system in our application:
1// middleware.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 middleware(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};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// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(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};In the Quantum Metropolis, different districts can have their own access systems. Similarly in Next.js, we can handle different domains and subdomains:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(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};In the Quantum Metropolis, the Gateway system optimizes data flow through its systems. Similarly, we can optimize our application:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(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};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// middleware.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 middleware(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};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// middleware.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 middleware(request: NextRequest) {
13 // We only limit API requests
14 if (request.nextUrl.pathname.startsWith('/api/')) {
15 const ip = request.ip || 'unknown';
16 const now = Date.now();
17
18 // Get or create a counter for this IP
19 if (!ipRequestCounts.has(ip) || ipRequestCounts.get(ip)!.resetAt < now) {
20 // Reset the counter if time has elapsed
21 ipRequestCounts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_DURATION });
22 } else {
23 // Increment counter
24 const record = ipRequestCounts.get(ip)!;
25 record.count++;
26
27 // Check if the limit has been exceeded
28 if (record.count > MAX_REQUESTS_PER_MINUTE) {
29 // Calculate the time after which requests can be sent again
30 const resetTime = Math.ceil((record.resetAt - now) / 1000);
31
32 // Return a response with 429 Too Many Requests code
33 return new NextResponse('Rate limit exceeded', {
34 status: 429,
35 headers: {
36 'Retry-After': `\${resetTime}`,
37 'X-RateLimit-Limit': `\${MAX_REQUESTS_PER_MINUTE}`,
38 'X-RateLimit-Remaining': '0',
39 'X-RateLimit-Reset': `\${resetTime}`,
40 },
41 });
42 }
43 }
44 }
45
46 return NextResponse.next();
47}
48
49export const config = {
50 matcher: '/api/:path*',
51};In the Quantum Metropolis, some new features are tested on selected groups of residents. Let's implement a similar A/B testing system:
1// middleware.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 middleware(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};In production applications, it's important to monitor middleware behavior. We can implement a simple logging system:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(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 // Calculate execution time
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};Middleware can also integrate with external systems, such as authentication or analytics systems:
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export async function middleware(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};Middleware in Next.js 15 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 15 application, adding advanced features that are executed early in the request lifecycle, allowing for greater control and better performance.
In the next chapter, we'll explore form handling and data validation techniques in Next.js 15, which will allow us to create interactive and user-friendly data collection interfaces.