Kurs Next.js · Moduł 2: Routing i layouty
Middleware i przechwytywanie żądań
W tej lekcji15
W sercu Metropolii Quantum znajduje się zaawansowany system kontroli dostępu i przekierowań, znany jako Quantum Gateway. Ten system monitoruje każdą próbę dostępu do różnych stref miasta, weryfikuje uprawnienia, przekierowuje do odpowiednich punktów i modyfikuje trasy podróży w czasie rzeczywistym. To niezwykle ważny element infrastruktury, który zapewnia zarówno bezpieczeństwo, jak i optymalizację ruchu w całym mieście.
W świecie Next.js 16 podobną rolę odgrywa Proxy (do wersji 15 nazywane Middleware) - potężny mechanizm, który pozwala na przechwytywanie i modyfikowanie żądań HTTP zanim zostaną one obsłużone przez stronę lub API, co umożliwia implementację takich funkcji jak autoryzacja, przekierowania, czy modyfikacje odpowiedzi.
Czym jest Proxy (dawniej Middleware) w Next.js 16?
Proxy to funkcja, która jest wykonywana przed obsługą żądania przez stronę lub API route. Działa ona między żądaniem klienta a odpowiedzią serwera, co daje możliwość:
- Wykonywania kodu przed załadowaniem strony
- Modyfikowania obiektu żądania
- Modyfikowania obiektu odpowiedzi
- Przekierowywania użytkownika
- Odrzucania żądań
- Dodawania nagłówków HTTP
- Modyfikowania cookies
Podstawowa struktura Middleware
W Next.js 16 ten kod umieszczasz w pliku proxy.ts (lub proxy.js) w katalogu głównym projektu (obok app lub pages, a w projekcie z katalogiem src w src). Do wersji 15 plik nazywał się middleware.ts, a eksportowana funkcja middleware: Next.js 16 zmienił obie nazwy, ale logika pozostała taka sama. Stary plik wciąż działa, jest jednak przestarzały, a proxy zawsze działa w środowisku Node.js.
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Funkcja proxy jest wykonywana przed każdym żądaniem
6export function proxy(request: NextRequest) {
7 // Tutaj możemy zaimplementować naszą logikę
8 console.log('Proxy uruchomione dla:', request.nextUrl.pathname);
9
10 // Możemy zmodyfikować obiekt odpowiedzi
11 return NextResponse.next();
12}
13
14// Opcjonalnie możemy ograniczyć, dla których tras proxy będzie uruchamiane
15export const config = {
16 matcher: '/api/:path*',
17};Dostępne operacje w Middleware
1. Przekierowania i przepisywanie adresów (rewrite)
Podobnie jak w Metropolii Quantum, gdzie bramy bezpieczeństwa mogą automatycznie przekierować nieautoryzowanych gości do punktów kontrolnych, middleware w Next.js pozwala na przekierowanie żądań:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Sprawdzenie, czy użytkownik próbuje dostać się do strefy administracyjnej
7 if (request.nextUrl.pathname.startsWith('/admin')) {
8 // Sprawdzamy, czy użytkownik jest zalogowany (np. sprawdzając cookie)
9 const isLoggedIn = request.cookies.has('auth-token');
10
11 if (!isLoggedIn) {
12 // Przekierowanie niezalogowanych użytkowników do strony logowania
13 const loginUrl = new URL('/login', request.url);
14 // Dodajemy oryginalny URL jako parametr, aby móc wrócić po zalogowaniu
15 loginUrl.searchParams.set('from', request.nextUrl.pathname);
16 return NextResponse.redirect(loginUrl);
17 }
18 }
19
20 // W przeciwnym razie kontynuujemy normalne przetwarzanie żądania
21 return NextResponse.next();
22}2. Modyfikowanie nagłówków żądania lub odpowiedzi
Możemy również modyfikować nagłówki, podobnie jak Quantum Gateway może dodawać specjalne identyfikatory do biletów transportowych:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Pobieramy oryginalną odpowiedź
7 const response = NextResponse.next();
8
9 // Dodajemy własne nagłówki do odpowiedzi
10 response.headers.set('x-quantum-security', 'enabled');
11 response.headers.set('x-quantum-gateway-version', '15.0.1');
12
13 // Dodanie polityki bezpieczeństwa treści (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. Manipulowanie cookies
Middleware może również manipulować cookies, co przydaje się przy zarządzaniu sesjami:
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 // Odczyt wartości cookie
9 const theme = request.cookies.get('theme')?.value;
10
11 // Ustawienie nowego cookie
12 response.cookies.set('last-visit', new Date().toISOString());
13
14 // Zmiana wartości istniejącego cookie
15 if (theme === 'dark') {
16 response.cookies.set('theme-version', 'dark-v2');
17 }
18
19 // Usunięcie cookie
20 if (request.nextUrl.pathname === '/logout') {
21 response.cookies.delete('auth-token');
22 }
23
24 return response;
25}4. Rewrite - podmiana URL bez zmiany widocznego adresu
Rewrite pozwala na podmianę adresu URL wewnętrznie, bez zmiany adresu widocznego dla użytkownika:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Przykład: wewnętrzne przekierowanie wszystkich żądań z /docs do /documentation
7 // Adres w pasku przeglądarki pozostaje /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}Implementacja Middleware dla aplikacji Quantum Voyages
Przygotujmy kompleksowe middleware dla naszej aplikacji Quantum Voyages, które będzie obsługiwać:
- Autoryzację dla stref chronionych
- Internacjonalizację i wykrywanie języka
- Śledzenie i analitykę
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { verifyAuthToken } from './lib/auth';
5
6// Lista obsługiwanych języków
7const SUPPORTED_LOCALES = ['pl', 'en', 'de', 'fr'];
8// Domyślny język
9const DEFAULT_LOCALE = 'pl';
10// Ścieżki wymagające autoryzacji
11const PROTECTED_PATHS = ['/dashboard', '/account', '/bookings'];
12// Ścieżki administracyjne wymagające uprawnień admina
13const ADMIN_PATHS = ['/admin'];
14
15export async function proxy(request: NextRequest) {
16 const { pathname } = request.nextUrl;
17
18 // 1. Obsługa lokalizacji i internacjonalizacji
19 const pathnameHasLocale = SUPPORTED_LOCALES.some(
20 locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
21 );
22
23 // Jeśli ścieżka nie zawiera jeszcze kodu języka, przekieruj i dodaj kod
24 if (!pathnameHasLocale) {
25 // Próba wykrycia preferowanego języka użytkownika
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 // Wyodrębnienie kodu języka z ścieżki
33 const locale = pathname.split('/')[1];
34
35 // 2. Autoryzacja dla stref chronionych
36 // Sprawdzenie, czy ścieżka jest chroniona
37 if (isProtectedPath(pathname)) {
38 const authToken = request.cookies.get('auth-token')?.value;
39
40 // Weryfikacja tokenu autoryzacji
41 const authResult = await verifyAuthToken(authToken);
42
43 // Jeśli użytkownik nie jest zalogowany lub token jest nieprawidłowy
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 // Sprawdzenie uprawnień admina dla ścieżek administracyjnych
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. Dodanie nagłówków analityki i bezpieczeństwa
58 const response = NextResponse.next();
59
60 // Dodanie informacji o użytkowniku dla narzędzi analitycznych (nie zawiera danych osobowych)
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 // Nagłówki bezpieczeństwa
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// Funkcje pomocnicze
77
78// Wykrywanie preferowanego języka użytkownika
79function getPreferredLocale(request: NextRequest): string {
80 // Sprawdzenie zapisanego wcześniej preferowanego języka w cookie
81 const savedLocale = request.cookies.get('preferred-locale')?.value;
82 if (savedLocale && SUPPORTED_LOCALES.includes(savedLocale)) {
83 return savedLocale;
84 }
85
86 // Sprawdzenie nagłówka Accept-Language
87 const acceptLanguage = request.headers.get('accept-language');
88 if (acceptLanguage) {
89 // Analiza nagłówka Accept-Language i wyszukanie najlepszego dopasowania
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 // Znajdź pierwszy obsługiwany język
99 for (const { locale } of userLocales) {
100 if (SUPPORTED_LOCALES.includes(locale)) {
101 return locale;
102 }
103 }
104 }
105
106 // Domyślny język, jeśli nie można określić preferencji
107 return DEFAULT_LOCALE;
108}
109
110// Sprawdzenie, czy ścieżka jest chroniona
111function isProtectedPath(pathname: string): boolean {
112 return PROTECTED_PATHS.some(path => {
113 // Usunięcie kodu języka z ścieżki przed porównaniem
114 const pathWithoutLocale = pathname.split('/').slice(2).join('/');
115 return `/${pathWithoutLocale}`.startsWith(path);
116 });
117}
118
119// Sprawdzenie, czy ścieżka jest administracyjna
120function isAdminPath(pathname: string): boolean {
121 return ADMIN_PATHS.some(path => {
122 // Usunięcie kodu języka z ścieżki przed porównaniem
123 const pathWithoutLocale = pathname.split('/').slice(2).join('/');
124 return `/${pathWithoutLocale}`.startsWith(path);
125 });
126}
127
128// Generowanie unikalnego ID dla odwiedzającego
129function generateVisitorId(): string {
130 return `${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
131}
132
133// Określenie, dla których ścieżek proxy będzie uruchamiane
134export const config = {
135 matcher: [
136 // Dopasowanie wszystkich ścieżek
137 '/((?!api|_next/static|_next/image|favicon.ico).*)',
138 ],
139};Selektywne uruchamianie Middleware z Matcher
Podobnie jak w Metropolii Quantum, gdzie nie każde przemieszczenie wymaga pełnej kontroli bezpieczeństwa, tak w Next.js 16 możemy określić, dla których ścieżek proxy będzie uruchamiane, używając konfiguracji matcher:
1// proxy.ts
2export const config = {
3 matcher: [
4 // Ścieżki, dla których proxy będzie uruchamiane
5 '/dashboard/:path*',
6 '/api/:path*',
7 '/((?!_next/static|_next/image|favicon.ico).*)',
8 ],
9};Dostępne są różne wzorce matcherów:
- Dopasowanie dokładne:
'/about' - Dopasowanie z parametrami:
'/blog/:path' - Dopasowanie z parametrami catch-all:
'/blog/:path*' - Kilka ścieżek naraz: tablica
['/dashboard/:path*', '/admin/:path*']albo grupa w nawiasach'/(dashboard|admin)/:path*' - Dopasowanie z negacją:
/((?!api|_next/static|_next/image|favicon.ico).*)
Implementacja autoryzacji JWT z Middleware
W Metropolii Quantum identyfikatory mieszkańców są weryfikowane przy użyciu zaawansowanej technologii kryptograficznej. Zaimplementujmy podobny system autoryzacji JWT w naszej aplikacji:
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 // Sprawdź, czy ścieżka wymaga autoryzacji
15 if (request.nextUrl.pathname.startsWith('/dashboard') ||
16 request.nextUrl.pathname.startsWith('/api/protected')) {
17
18 // Pobierz token JWT z cookie lub nagłówka Authorization
19 const authToken = request.cookies.get('auth-token')?.value ||
20 request.headers.get('Authorization')?.split(' ')[1];
21
22 if (!authToken) {
23 // Brak tokenu - przekierowanie do logowania
24 return redirectToLogin(request);
25 }
26
27 try {
28 // Weryfikacja tokenu JWT
29 const secretKey = new TextEncoder().encode(process.env.JWT_SECRET!);
30 const { payload } = await jwtVerify<UserPayload>(authToken, secretKey);
31
32 // Sprawdzenie, czy token nie wygasł
33 const currentTime = Math.floor(Date.now() / 1000);
34 if (payload.exp < currentTime) {
35 // Token wygasł - przekierowanie do logowania
36 return redirectToLogin(request);
37 }
38
39 // Sprawdzenie uprawnień admina dla ścieżek administracyjnych
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 // Dodanie informacji o użytkowniku do nagłówków dla kolejnych obsług
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 // Błąd weryfikacji tokenu - przekierowanie do logowania
55 return redirectToLogin(request);
56 }
57 }
58
59 return NextResponse.next();
60}
61
62// Funkcja przekierowująca do logowania
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};Implementacja przełączania motywów z Middleware
Quantum City oferuje swoim mieszkańcom możliwość dostosowania wyglądu interfejsów miejskich. Zaimplementujmy podobny system przełączania między jasnymi i ciemnymi motywami:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function proxy(request: NextRequest) {
6 // Pobierz preferowany motyw
7 const theme = request.cookies.get('theme')?.value || 'light';
8
9 // Utwórz odpowiedź
10 const response = NextResponse.next();
11
12 // Ustaw zmienną preferencji motywu jako cookie
13 response.cookies.set('theme', theme, {
14 maxAge: 60 * 60 * 24 * 365, // ważne przez rok
15 path: '/',
16 });
17
18 // Dodaj niestandardowy nagłówek, który może być odczytany przez klienta
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};Obsługa wielu domen i subdomen z Middleware
W Metropolii Quantum różne dzielnice mogą mieć swoje własne systemy dostępu. Podobnie w Next.js możemy obsługiwać różne domeny i subdomeny:
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 // Obsługa różnych subdomen
9 if (hostname === 'admin.quantumvoyages.com') {
10 // Przekierowanie wszystkich żądań do panelu administracyjnego
11 const url = new URL('/admin' + pathname, request.url);
12 return NextResponse.rewrite(url);
13 }
14
15 if (hostname === 'api.quantumvoyages.com') {
16 // Przekierowanie żądań do API
17 const url = new URL('/api' + pathname, request.url);
18 return NextResponse.rewrite(url);
19 }
20
21 if (hostname.startsWith('user-')) {
22 // Obsługa subdomen użytkowników (np. 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};Cachowanie i modyfikacja nagłówków odpowiedzi
W Metropolii Quantum system Gateway optymalizuje przepływ danych przez swoje systemy. Podobnie możemy zoptymalizować naszą aplikację:
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 // Ustawienie nagłówków cachowania dla zasobów statycznych
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 // Ustawienie nagłówków cachowania dla stron HTML
14 else if (!request.nextUrl.pathname.startsWith('/api/')) {
15 response.headers.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
16 }
17
18 // Dodanie nagłówków bezpieczeństwa
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};Implementacja geolokalizacji i personalizacji treści
Metropolia Quantum dostosowuje dostępne usługi w zależności od położenia mieszkańców. Zaimplementujmy podobny system w naszej aplikacji, używając middleware do określenia lokalizacji użytkownika:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Tabela regionów i ich dostępnych destynacji
6const REGION_DESTINATIONS = {
7 'EU': ['mars', 'europa', 'titan'],
8 'NA': ['mars', 'luna', 'titan', 'ganymede'],
9 'ASIA': ['mars', 'venus-orbiter', 'luna'],
10 'default': ['mars'] // domyślne destynacje dostępne globalnie
11};
12
13export function proxy(request: NextRequest) {
14 // Pobierz region użytkownika (w rzeczywistości można użyć zewnętrznego API do geolokalizacji)
15 const countryCode = request.headers.get('x-country') || 'default';
16 const region = getRegionFromCountry(countryCode);
17
18 // Utwórz odpowiedź
19 const response = NextResponse.next();
20
21 // Dodanie informacji o regionie i dostępnych destynacjach jako nagłówki
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 // Ustawienie cookie z regionem (będzie dostępne w przeglądarce)
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// Funkcja mapująca kody krajów na regiony
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};Ograniczanie liczby żądań (Rate limiting)
W Metropolii Quantum system Gateway kontroluje ilość zapytań do krytycznych zasobów, aby zapobiec przeciążeniu. Zaimplementujmy podobny mechanizm rate limitingu:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Prosty system rate limitingu oparty na pamięci (w produkcji lepiej użyć Redis lub podobnej usługi)
6const RATE_LIMIT_DURATION = 60 * 1000; // 1 minuta
7const MAX_REQUESTS_PER_MINUTE = 60; // Maksymalna liczba żądań na minutę
8
9// Przechowywanie liczników żądań dla różnych IP (w rzeczywistej aplikacji lepiej użyć Redis)
10const ipRequestCounts = new Map<string, { count: number, resetAt: number }>();
11
12export function proxy(request: NextRequest) {
13 // Ograniczamy tylko żądania API
14 if (request.nextUrl.pathname.startsWith('/api/')) {
15 // NextRequest nie ma już pola ip (usunięte w Next.js 15) - adres klienta podaje nagłówek x-forwarded-for
16 const ip = request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || 'unknown';
17 const now = Date.now();
18
19 // Pobierz lub utwórz licznik dla tego IP
20 if (!ipRequestCounts.has(ip) || ipRequestCounts.get(ip)!.resetAt < now) {
21 // Zresetuj licznik, jeśli upłynął czas
22 ipRequestCounts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_DURATION });
23 } else {
24 // Zwiększ licznik
25 const record = ipRequestCounts.get(ip)!;
26 record.count++;
27
28 // Sprawdź, czy przekroczono limit
29 if (record.count > MAX_REQUESTS_PER_MINUTE) {
30 // Oblicz czas, po którym można ponownie wysyłać żądania
31 const resetTime = Math.ceil((record.resetAt - now) / 1000);
32
33 // Zwróć odpowiedź z kodem 429 Too Many Requests
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};Obsługa A/B testów z Middleware
W Metropolii Quantum niektóre nowe funkcje są testowane na wybranych grupach mieszkańców. Zaimplementujmy podobny system A/B testów:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Definicje testów A/B
6const AB_TESTS = {
7 'new-landing-page': {
8 variants: ['control', 'variant-a', 'variant-b'],
9 weights: [0.5, 0.25, 0.25], // 50% użytkowników zobaczy control, 25% variant-a, 25% variant-b
10 },
11 'checkout-flow': {
12 variants: ['standard', 'simplified'],
13 weights: [0.5, 0.5], // 50% użytkowników zobaczy każdy wariant
14 },
15};
16
17export function proxy(request: NextRequest) {
18 const response = NextResponse.next();
19
20 // Przypisz użytkownika do grup testów
21 Object.entries(AB_TESTS).forEach(([testName, test]) => {
22 // Pobierz istniejący wariant z cookie lub przypisz nowy
23 let variant = request.cookies.get(`ab-${testName}`)?.value;
24
25 if (!variant || !test.variants.includes(variant)) {
26 // Przypisz wariant na podstawie wag
27 variant = assignVariant(test.variants, test.weights);
28
29 // Zapisz wariant w cookie
30 response.cookies.set(`ab-${testName}`, variant, {
31 maxAge: 60 * 60 * 24 * 30, // 30 dni
32 path: '/',
33 });
34 }
35
36 // Dodaj informację o wariancie jako nagłówek dla klienta
37 response.headers.set(`x-ab-${testName}`, variant);
38 });
39
40 return response;
41}
42
43// Funkcja przypisująca wariant na podstawie wag
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 // Domyślnie zwróć pierwszy wariant
56 return variants[0];
57}
58
59export const config = {
60 matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
61};Monitorowanie i debugowanie Middleware
W zastosowaniach produkcyjnych ważne jest monitorowanie działania middleware. Możemy zaimplementować prosty system logowania:
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 // Utwórz identyfikator żądania
9 const requestId = generateRequestId();
10
11 // Utwórz odpowiedź
12 const response = NextResponse.next();
13
14 // Dodaj identyfikator żądania do nagłówków
15 response.headers.set('x-request-id', requestId);
16
17 // Czas działania samego proxy (strona renderuje się dopiero po nim, więc to nie jest czas całego żądania)
18 const duration = Date.now() - startTime;
19 response.headers.set('x-response-time', `${duration}ms`);
20
21 // W środowisku produkcyjnym można wysłać te dane do systemu monitorowania
22 // logRequest(requestId, request.method, request.url, response.status, duration);
23
24 return response;
25}
26
27// Generowanie unikalnego identyfikatora żądania
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};Integracja z systemami zewnętrznymi
Middleware może również integrować się z zewnętrznymi systemami, takimi jak systemy uwierzytelniania lub analityki:
1// proxy.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export async function proxy(request: NextRequest) {
6 // Pobierz informacje o sesji z zewnętrznego serwisu
7 const sessionId = request.cookies.get('session-id')?.value;
8 let userInfo = null;
9
10 if (sessionId) {
11 try {
12 // W rzeczywistej aplikacji, to mogłoby być wywołanie API
13 userInfo = await fetchUserInfo(sessionId);
14 } catch (error) {
15 console.error('Error fetching user info:', error);
16 }
17 }
18
19 // Utwórz odpowiedź
20 const response = NextResponse.next();
21
22 // Jeśli mamy informacje o użytkowniku, dodaj je do nagłówków
23 if (userInfo) {
24 response.headers.set('x-user-id', userInfo.id);
25 response.headers.set('x-user-plan', userInfo.plan);
26
27 // Wysyłanie danych do systemu analityki
28 trackUserActivity(userInfo.id, request.url, request.method);
29 }
30
31 return response;
32}
33
34// Funkcja pobierająca informacje o użytkowniku (symulacja)
35async function fetchUserInfo(sessionId: string) {
36 // W rzeczywistej aplikacji, mogłoby to być wywołanie API
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// Funkcja śledząca aktywność użytkownika (symulacja)
49function trackUserActivity(userId: string, url: string, method: string) {
50 // W rzeczywistej aplikacji, mogłoby to wysyłać dane do Google Analytics, Mixpanel, itp.
51 console.log(`User ${userId} accessed ${url} with method ${method}`);
52}
53
54export const config = {
55 matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
56};Podsumowanie
Proxy w Next.js 16 (dawniej Middleware) jest potężnym narzędziem, które, podobnie jak Quantum Gateway w Metropolii Quantum, umożliwia kontrolę i modyfikację ruchu w aplikacji na samym początku cyklu żądanie-odpowiedź. Dzięki temu możemy:
Implementować zaawansowane systemy autoryzacji - weryfikować tokeny JWT, przekierowywać niezalogowanych użytkowników, kontrolować dostęp do chronionych zasobów.
Dodawać funkcje internacjonalizacji - wykrywać preferowany język użytkownika, przekierowywać do odpowiednich wersji językowych.
Personalizować doświadczenie użytkownika - dostosowywać treści na podstawie lokalizacji, preferencji i historii użytkownika.
Optymalizować wydajność - kontrolować nagłówki cachowania, modyfikować odpowiedzi, implementować rate limiting.
Integrować się z zewnętrznymi systemami - łączyć się z systemami analityki, uwierzytelniania i innymi usługami.
Przeprowadzać testy A/B - przydzielać użytkowników do grup testowych i personalizować doświadczenie.
Monitorować i debugować - śledzić żądania, mierzyć wydajność, zapisywać metryki.
Wykorzystując middleware, możemy znacząco rozszerzyć możliwości naszej aplikacji Next.js 16, dodając zaawansowane funkcje, które są wykonywane na wczesnym etapie cyklu życia żądania, co pozwala na większą kontrolę i lepszą wydajność.
W następnej lekcji poznasz prefetching i inne sposoby przyspieszania nawigacji w Next.js 16.
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Metadata w App Routerze definiuje się przez:
2. Private Folders w App Routerze oznacza się prefiksem:
3. Komponenty w App Routerze bez dyrektywy 'use client' domyślnie:
4. Aby stworzyć Client Component należy dodać:
5. Czym jest prefetching w Next.js?
6. Jak w Next.js 16 musi się nazywać plik w katalogu głównym projektu, który zastąpił middleware.ts i uruchamia się przed obsługą żądania?
7. Czym różni się router.replace() od router.push() z hooka useRouter?
8. Jaki prefiks musi mieć zmienna środowiskowa, aby była dostępna po stronie klienta w Next.js?
9. Parallel Routes w App Routerze używają konwencji nazewnictwa:
10. Co się stanie, gdy w Next.js 16 wywołasz funkcję notFound() wewnątrz Server Component?
11. Dlaczego plik error.tsx w Next.js 16 musi być Client Component (z dyrektywą 'use client')?
12. Do czego najlepiej nadaje się plik template.tsx zamiast layout.tsx?
Zadania praktyczne w grze
- Edytor kodu
Utwórz catch-all route app/docs/[...slug]/page.js: wyeksportuj domyślnie async komponent DocsPage({ params }), odczytaj tablicę segmentów przez const { slug } = await params i wyświetl każdy segment URL w osobnym elemencie li listy ul.
- Układanie w pionie
Ułóż kolejność obsługi błędu przez plik error.js
- Klikanie w kolejności
Ułóż prawidłową ścieżkę do pliku catch-all route w Next.js
- Układanie w pionie
Ułóż składnię root layoutu (app/layout.tsx) w App Routerze
- Układanie w pionie
Ułóż kolejność generowania statycznych stron w App Routerze
- Układanie w pionie
Ułóż składnię root layout z tagami html i body
- Klikanie w kolejności
Ułóż prawidłową ścieżkę do dynamicznej strony produktu w App Routerze
- Układanie w pionie
Ułóż kolejność procesu Incremental Static Regeneration
- Klikanie w kolejności
Ułóż składnię usePathname w komponencie
- Układanie w pionie
Ułóż kroki nawigacji z komponentem Link: od pojawienia się linku na ekranie do aktualizacji strony
- Układanie w pionie
Ułóż kolejność rozwiązywania metadata w Next.js
- Układanie w pionie
Ułóż składnię importu i użycia hooka useRouter
- Klikanie w kolejności
Ułóż składnię generateMetadata
- Układanie w pionie
Ułóż kolejność optymalizacji fontu z next/font (od budowania do wyświetlenia tekstu)
- Układanie w pionie
Ułóż składnię użycia komponentu Script z Next.js
- Układanie w pionie
Ułóż kolejność renderowania Server Components