Kurs Next.js · Moduł 2: Routing i layouty

Middleware i przechwytywanie żądań

16 min czytania
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ć:

  1. Autoryzację dla stref chronionych
  2. Internacjonalizację i wykrywanie języka
  3. Ś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:

  1. Dopasowanie dokładne: '/about'
  2. Dopasowanie z parametrami: '/blog/:path'
  3. Dopasowanie z parametrami catch-all: '/blog/:path*'
  4. Kilka ścieżek naraz: tablica ['/dashboard/:path*', '/admin/:path*'] albo grupa w nawiasach '/(dashboard|admin)/:path*'
  5. 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:

  1. Implementować zaawansowane systemy autoryzacji - weryfikować tokeny JWT, przekierowywać niezalogowanych użytkowników, kontrolować dostęp do chronionych zasobów.

  2. Dodawać funkcje internacjonalizacji - wykrywać preferowany język użytkownika, przekierowywać do odpowiednich wersji językowych.

  3. Personalizować doświadczenie użytkownika - dostosowywać treści na podstawie lokalizacji, preferencji i historii użytkownika.

  4. Optymalizować wydajność - kontrolować nagłówki cachowania, modyfikować odpowiedzi, implementować rate limiting.

  5. Integrować się z zewnętrznymi systemami - łączyć się z systemami analityki, uwierzytelniania i innymi usługami.

  6. Przeprowadzać testy A/B - przydzielać użytkowników do grup testowych i personalizować doświadczenie.

  7. 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. 1. Metadata w App Routerze definiuje się przez:

  2. 2. Private Folders w App Routerze oznacza się prefiksem:

  3. 3. Komponenty w App Routerze bez dyrektywy 'use client' domyślnie:

  4. 4. Aby stworzyć Client Component należy dodać:

  5. 5. Czym jest prefetching w Next.js?

  6. 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. 7. Czym różni się router.replace() od router.push() z hooka useRouter?

  8. 8. Jaki prefiks musi mieć zmienna środowiskowa, aby była dostępna po stronie klienta w Next.js?

  9. 9. Parallel Routes w App Routerze używają konwencji nazewnictwa:

  10. 10. Co się stanie, gdy w Next.js 16 wywołasz funkcję notFound() wewnątrz Server Component?

  11. 11. Dlaczego plik error.tsx w Next.js 16 musi być Client Component (z dyrektywą 'use client')?

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

Przydatne artykuły