W świecie nowoczesnych aplikacji webowych wydajność jest kluczowym czynnikiem wpływającym na doświadczenie użytkownika i sukces biznesowy. Next.js oferuje zaawansowane narzędzia do optymalizacji wydajności, wśród których Edge Runtime wysuwa się na pierwszy plan jako potężne rozwiązanie. W tym module omowimy Edge Runtime oraz inne strategie optymalizacji wydajności w aplikacjach Next.js.
Edge Runtime to lekkie środowisko wykonawcze, które umożliwia uruchamianie kodu bliżej użytkownika końcowego - na "brzegu" sieci - zamiast w centralnym centrum danych. Next.js umożliwia uruchamianie Route Handlerów i stron w Edge Runtime, co zapewnia:
Warto rozumieć różnice między Edge Runtime a tradycyjnym środowiskiem Node.js:
| Cecha | Edge Runtime | Node.js Runtime | |-------|--------------|----------------| | Czas zimnego startu | Bardzo szybki (milisekundy) | Wolniejszy (setki milisekund) | | Skalowalność | Automatyczne, globalne | Wymaga konfiguracji | | Dostęp do API | Ograniczony zestaw | Pełne API Node.js | | Rozmiar kodu | Limitowany (zazwyczaj 1-4 MB) | Praktycznie nieograniczony | | Dostęp do systemu plików | Brak | Pełny dostęp | | Czas wykonania funkcji | Ograniczony (np. 50-60 ms) | Dłuższy (minuty) | | Typowe zastosowania | API, strony z dynamicznym contentem | Skomplikowane operacje, dostęp do baz danych |
Aby uruchomić komponent strony lub Route Handler w Edge Runtime, używamy eksportu
runtime w pliku:1// app/api/edge-route/route.ts
2import { NextRequest } from 'next/server';
3
4export const runtime = 'edge'; // Deklaracja Edge Runtime
5
6export async function GET(request: NextRequest) {
7 const { searchParams } = new URL(request.url);
8 const query = searchParams.get('query') || 'Świat';
9
10 // Obsługa żądania przy minimalnym opóźnieniu
11 return Response.json({ message: `Witaj, ${query}!` });
12}Dla strony lub komponentu Layoutu:
1// app/edge-page/page.tsx
2import { cookies } from 'next/headers';
3
4export const runtime = 'edge';
5
6export default async function EdgePage() {
7 const cookie = (await cookies()).get('theme')?.value || 'light';
8
9 return (
10 <div>
11 <h1>Strona renderowana w Edge Runtime</h1>
12 <p>Twój motyw: {cookie}</p>
13 </div>
14 );
15}Edge Runtime jest idealny dla:
Przykład geolokalizacji w Edge Runtime:
1// app/edge-geo/page.tsx
2import { headers } from 'next/headers';
3
4export const runtime = 'edge';
5
6export default async function GeoPage() {
7 const headersList = await headers();
8 const countryCode = headersList.get('x-vercel-ip-country') || 'Unknown';
9 const city = headersList.get('x-vercel-ip-city') || 'Unknown City';
10
11 return (
12 <div>
13 <h1>Witaj, odwiedzający!</h1>
14 <p>Wykryliśmy, że jesteś z: {city}, {countryCode}</p>
15 {countryCode === 'PL' && (
16 <p>Specjalna treść dla odwiedzających z Polski!</p>
17 )}
18 </div>
19 );
20}Next.js oferuje wbudowane rozwiązania do optymalizacji mediów, które znacząco wpływają na wydajność aplikacji.
Komponent
Image w Next.js automatycznie optymalizuje obrazy, co zmniejsza rozmiar przesyłanych danych i poprawia metryki Web Vitals:1import Image from 'next/image';
2
3export default function OptimizedImages() {
4 return (
5 <div>
6 <h2>Optymalizowane obrazy w Next.js</h2>
7
8 <Image
9 src="/hero-image.jpg" // Ścieżka względna w katalogu public
10 alt="Hero Image"
11 width={1200}
12 height={600}
13 priority // Priorytet ładowania dla obrazów nad zagięciem
14 />
15
16 <Image
17 src="https://cdn.example.com/remote-image.jpg" // Zdalny obraz
18 alt="Remote Image"
19 width={800}
20 height={400}
21 quality={80} // Regulacja jakości (1-100)
22 placeholder="blur"
23 blurDataURL="data:image/svg+xml;base64,..." // Opcjonalny placeholder podczas ładowania
24 />
25 </div>
26 );
27}Kluczowe funkcje komponentu
Image:Next.js App Router wprowadza nowy system ładowania czcionek, który eliminuje problemy z migotaniem tekstu (CLS):
1// app/layout.tsx
2import { Inter, Roboto_Mono } from 'next/font/google';
3
4// Konfiguracja czcionki zmiennej (variable font)
5const inter = Inter({
6 subsets: ['latin', 'latin-ext'],
7 display: 'swap',
8});
9
10// Konfiguracja czcionki o stałej grubości
11const robotoMono = Roboto_Mono({
12 subsets: ['latin'],
13 display: 'swap',
14 weight: ['400', '700'],
15});
16
17export default function RootLayout({ children }) {
18 return (
19 <html lang="pl" className={`${inter.className} ${robotoMono.variable}`}>
20 <body>{children}</body>
21 </html>
22 );
23}W komponencie możemy użyć zdefiniowanej czcionki:
1// app/page.tsx
2export default function Home() {
3 return (
4 <main>
5 <h1 className={inter.className}>Nagłówek w czcionce Inter</h1>
6 <code className="font-mono">// To będzie w Roboto Mono dzięki zmiennej CSS</code>
7 </main>
8 );
9}React Server Components (RSC) pozwalają znacząco zmniejszyć ilość JavaScript wysyłanego do przeglądarki:
1// app/products/page.tsx - Server Component (domyślnie)
2async function getProducts() {
3 const res = await fetch('https://api.example.com/products');
4 return res.json();
5}
6
7export default async function ProductsPage() {
8 // Ten kod NIGDY nie jest wysyłany do klienta
9 const products = await getProducts();
10 const totalValue = products.reduce((sum, product) => sum + product.price, 0);
11
12 return (
13 <div>
14 <h1>Produkty (wartość: {totalValue} zł)</h1>
15 <ProductList products={products} />
16 </div>
17 );
18}Streaming umożliwia stopniowe przesyłanie UI, co przyspiesza Time to First Byte i poprawia interaktywność:
1// app/dashboard/page.tsx
2import { Suspense } from 'react';
3import Loading from './loading';
4
5// Komponenty ładujące dane niezależnie od siebie
6import UserProfile from './UserProfile';
7import RecentOrders from './RecentOrders';
8import Analytics from './Analytics';
9
10export default function Dashboard() {
11 return (
12 <div className="dashboard">
13 <h1>Panel kontrolny</h1>
14
15 <div className="dashboard-grid">
16 {/* Szybciej ładujące się elementy będą widoczne od razu */}
17 <Suspense fallback={<Loading section="profile" />}>
18 <UserProfile />
19 </Suspense>
20
21 <Suspense fallback={<Loading section="orders" />}>
22 <RecentOrders />
23 </Suspense>
24
25 <Suspense fallback={<Loading section="analytics" />}>
26 <Analytics />
27 </Suspense>
28 </div>
29 </div>
30 );
31}Next.js App Router automatycznie prefetchuje linki widoczne w widoku, co można dodatkowo kontrolować:
1import Link from 'next/link';
2
3export default function Navigation() {
4 return (
5 <nav>
6 {/* Domyślny prefetch */}
7 <Link href="/products">Produkty</Link>
8
9 {/* Wyłączony prefetch */}
10 <Link href="/heavy-page" prefetch={false}>Ciężka strona</Link>
11
12 {/* Prefetch tylko nagłówków (dla dynamicznych stron) */}
13 <Link href="/blog/article-123">Artykuł</Link>
14 </nav>
15 );
16}Parallel Routes umożliwiają ładowanie wielu sekcji strony niezależnie od siebie:
1// app/layout.tsx
2export default function Layout({ children, sidebar }) {
3 return (
4 <div className="layout">
5 <aside className="sidebar">
6 {sidebar}
7 </aside>
8 <main>
9 {children}
10 </main>
11 </div>
12 );
13}
14
15// app/@sidebar/page.tsx - ładowany niezależnie
16export default function Sidebar() {
17 return <div>Zawartość sidebara</div>;
18}
19
20// app/page.tsx - główna zawartość
21export default function Home() {
22 return <div>Główna zawartość</div>;
23}Route Handlers mogą być zoptymalizowane pod kątem wydajności, szczególnie w połączeniu z Edge Runtime:
1// app/api/optimized/route.ts
2import { NextResponse } from 'next/server';
3
4export const runtime = 'edge';
5
6export async function GET() {
7 // Stosowanie CORS dla optymalizacji preflightów
8 const response = NextResponse.json({ data: 'wartość' });
9
10 response.headers.set('Access-Control-Allow-Origin', '*');
11 response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS');
12 response.headers.set('Access-Control-Allow-Headers', 'Content-Type');
13
14 // Ustawienie Cache-Control dla browsera
15 response.headers.set(
16 'Cache-Control',
17 'public, max-age=60, s-maxage=3600, stale-while-revalidate=600'
18 );
19
20 return response;
21}Funkcja
generateStaticParams służy do generowania statycznych wersji stron z parametrami:1// app/products/[category]/[id]/page.tsx
2export async function generateStaticParams() {
3 // Pobierz tylko niezbędne dane dla generowania ścieżek
4 const categories = await fetch('https://api.example.com/categories').then(r => r.json());
5
6 // Generuj kombinacje parametrów dla najważniejszych stron
7 const paths = [];
8
9 for (const category of categories) {
10 // Pobierz tylko top produkty dla każdej kategorii
11 const topProducts = await fetch(
12 `https://api.example.com/categories/${category.id}/top-products`
13 ).then(r => r.json());
14
15 for (const product of topProducts) {
16 paths.push({
17 category: category.slug,
18 id: product.id.toString(),
19 });
20 }
21 }
22
23 return paths;
24}
25
26export default async function ProductPage({ params }) {
27 const { category, id } = await params;
28 // Strona produktu...
29}Optymalizacja polega na:
Next.js obsługuje różne strategie ładowania CSS, które można optymalizować:
1// app/products/[id]/page.tsx
2import './product-page.css'; // Globalny CSS dla tej trasy
3
4// CSS Modules dla komponentu
5import styles from './ProductDetails.module.css';
6
7// Styled JSX (wbudowana opcja)
8function ProductColorChoice() {
9 return (
10 <div className="color-selector">
11 <style jsx>{`
12 .color-selector {
13 display: flex;
14 gap: 8px;
15 }
16 .color-option {
17 width: 24px;
18 height: 24px;
19 border-radius: 50%;
20 cursor: pointer;
21 }
22 `}</style>
23 <div className="color-option" style={{ backgroundColor: 'red' }} />
24 <div className="color-option" style={{ backgroundColor: 'blue' }} />
25 </div>
26 );
27}
28
29export default async function ProductPage({ params }) {
30 return (
31 <div className={styles.productContainer}>
32 <h1 className={styles.productTitle}>Szczegóły produktu {(await params).id}</h1>
33 <ProductColorChoice />
34 </div>
35 );
36}Optymalizacja CSS:
Hosting Next.js na platformach obsługujących nowoczesne protokoły HTTP/2 i HTTP/3 zapewnia lepszą wydajność dzięki:
Nie wymaga to zmian w kodzie, ale odpowiedniej konfiguracji serwera lub wyboru platformy hostingowej (np. Vercel, Netlify, Cloudflare Pages).
Next.js oferuje wbudowane metryki Web Vitals, które możemy śledzić:
1// instrumentation.ts (w katalogu głównym)
2export function register() {
3 if (typeof window !== 'undefined') {
4 // Rozwiązanie działa tylko po stronie klienta
5 import('web-vitals').then(({ onCLS, onFID, onLCP, onTTFB }) => {
6 onCLS(metric => sendAnalyticsEvent('CLS', metric));
7 onFID(metric => sendAnalyticsEvent('FID', metric));
8 onLCP(metric => sendAnalyticsEvent('LCP', metric));
9 onTTFB(metric => sendAnalyticsEvent('TTFB', metric));
10 });
11 }
12}
13
14function sendAnalyticsEvent(name, metric) {
15 const body = JSON.stringify({
16 name,
17 value: metric.value,
18 page: window.location.pathname,
19 id: metric.id,
20 });
21
22 // Wysyłka do własnego API
23 navigator.sendBeacon('/api/analytics', body);
24}Analiza rozmiaru bundla jest kluczowa dla wydajności. Next.js oferuje wbudowaną analizę bundla:
1next build --analyzeMożemy też użyć bardziej zaawansowanych narzędzi jak
@next/bundle-analyzer:1// next.config.js
2const withBundleAnalyzer = require('@next/bundle-analyzer')({
3 enabled: process.env.ANALYZE === 'true',
4});
5
6module.exports = withBundleAnalyzer({
7 // Konfiguracja Next.js
8});Następnie uruchamiamy:
1ANALYZE=true npm run buildW Metropolii Quantum roku 2150, bezpieczeństwo to nie opcja - to konieczność. Każda luka w systemie może być wykorzystana przez cyberprzestępców czy wrogie korporacje. W tej sekcji omówimy najważniejsze praktyki zabezpieczania aplikacji Next.js, które chronią zarówno dane użytkowników, jak i integralność całego systemu.
CSRF to atak, w którym złośliwa strona wykorzystuje uwierzytelnioną sesję użytkownika do wykonania nieautoryzowanych akcji. Next.js oferuje wbudowane mechanizmy ochrony, szczególnie w Server Actions.
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6 // Weryfikacja Origin header dla mutacji
7 if (request.method !== 'GET' && request.method !== 'HEAD') {
8 const origin = request.headers.get('origin');
9 const host = request.headers.get('host');
10
11 // Sprawdź czy Origin odpowiada hostowi
12 if (origin && !origin.includes(host || '')) {
13 return new NextResponse('Forbidden', { status: 403 });
14 }
15 }
16
17 return NextResponse.next();
18}
19
20export const config = {
21 matcher: '/api/:path*'
22};1// lib/csrf.ts
2import { cookies } from 'next/headers';
3import crypto from 'crypto';
4
5export async function generateCSRFToken(): Promise<string> {
6 const token = crypto.randomBytes(32).toString('hex');
7 const cookieStore = await cookies();
8
9 cookieStore.set('csrf-token', token, {
10 httpOnly: true,
11 secure: process.env.NODE_ENV === 'production',
12 sameSite: 'strict',
13 maxAge: 60 * 60 // 1 godzina
14 });
15
16 return token;
17}
18
19export async function verifyCSRFToken(token: string): Promise<boolean> {
20 const cookieStore = await cookies();
21 const storedToken = cookieStore.get('csrf-token')?.value;
22
23 return storedToken === token;
24}
25
26// app/actions.ts
27'use server';
28
29import { verifyCSRFToken } from '@/lib/csrf';
30
31export async function submitForm(formData: FormData) {
32 const csrfToken = formData.get('csrf-token') as string;
33
34 if (!await verifyCSRFToken(csrfToken)) {
35 throw new Error('Invalid CSRF token');
36 }
37
38 // Przetwarzanie formularza...
39}
40
41// app/my-form/page.tsx
42import { generateCSRFToken } from '@/lib/csrf';
43import { submitForm } from '@/app/actions';
44
45export default async function MyFormPage() {
46 const csrfToken = await generateCSRFToken();
47
48 return (
49 <form action={submitForm}>
50 <input type="hidden" name="csrf-token" value={csrfToken} />
51 {/* Pozostałe pola formularza */}
52 <button type="submit">Wyślij</button>
53 </form>
54 );
55}XSS to atak, w którym napastnik wstrzykuje złośliwy kod JavaScript do aplikacji. Next.js i React oferują wbudowaną ochronę, ale warto znać dodatkowe techniki.
React automatycznie escapuje wszystkie wartości renderowane w JSX:
1// Bezpieczne - React automatycznie escapuje
2function UserProfile({ username }: { username: string }) {
3 // Nawet jeśli username = "<script>alert('xss')</script>"
4 // React wyświetli to jako tekst, nie wykona
5 return <div>Witaj, {username}!</div>;
6}1// NIEBEZPIECZNE - może wykonać złośliwy kod
2function DangerousComponent({ html }: { html: string }) {
3 return <div dangerouslySetInnerHTML={{ __html: html }} />;
4}
5
6// BEZPIECZNE - użyj biblioteki do sanityzacji
7import DOMPurify from 'isomorphic-dompurify';
8
9function SafeComponent({ html }: { html: string }) {
10 const sanitized = DOMPurify.sanitize(html, {
11 ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'ul', 'ol', 'li'],
12 ALLOWED_ATTR: ['href', 'target']
13 });
14
15 return <div dangerouslySetInnerHTML={{ __html: sanitized }} />;
16}CSP to mechanizm zabezpieczający przed XSS poprzez określenie, jakie źródła kodu są zaufane:
1// next.config.js
2const ContentSecurityPolicy = `
3 default-src 'self';
4 script-src 'self' 'unsafe-eval' 'unsafe-inline' https://trusted-cdn.com;
5 style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
6 img-src 'self' data: https: blob:;
7 font-src 'self' https://fonts.gstatic.com;
8 connect-src 'self' https://api.example.com;
9 frame-ancestors 'none';
10`;
11
12const securityHeaders = [
13 {
14 key: 'Content-Security-Policy',
15 value: ContentSecurityPolicy.replace(/\s{2,}/g, ' ').trim()
16 }
17];
18
19module.exports = {
20 async headers() {
21 return [
22 {
23 source: '/:path*',
24 headers: securityHeaders
25 }
26 ];
27 }
28};Lub przez middleware:
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 const csp = [
9 "default-src 'self'",
10 "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
11 "style-src 'self' 'unsafe-inline'",
12 "img-src 'self' data: https:",
13 "font-src 'self' data:",
14 "connect-src 'self' https://api.example.com"
15 ].join('; ');
16
17 response.headers.set('Content-Security-Policy', csp);
18
19 return response;
20}Rate limiting chroni aplikację przed atakami typu DDoS i nadużyciami API. Możemy zaimplementować to w Next.js Middleware:
1// lib/rate-limit.ts
2import { LRUCache } from 'lru-cache';
3
4type RateLimitOptions = {
5 interval: number; // czas w milisekundach
6 uniqueTokenPerInterval: number; // maksymalna liczba unikalnych tokenów
7};
8
9export function rateLimit(options: RateLimitOptions) {
10 const tokenCache = new LRUCache({
11 max: options.uniqueTokenPerInterval || 500,
12 ttl: options.interval || 60000
13 });
14
15 return {
16 check: (limit: number, token: string): { success: boolean; remaining: number } => {
17 const tokenCount = (tokenCache.get(token) as number[]) || [0];
18
19 if (tokenCount[0] === 0) {
20 tokenCache.set(token, [1]);
21 return { success: true, remaining: limit - 1 };
22 }
23
24 const currentCount = tokenCount[0];
25
26 if (currentCount >= limit) {
27 return { success: false, remaining: 0 };
28 }
29
30 tokenCache.set(token, [currentCount + 1]);
31 return { success: true, remaining: limit - currentCount - 1 };
32 }
33 };
34}
35
36// middleware.ts
37import { NextResponse } from 'next/server';
38import type { NextRequest } from 'next/server';
39import { rateLimit } from '@/lib/rate-limit';
40
41const limiter = rateLimit({
42 interval: 60 * 1000, // 1 minuta
43 uniqueTokenPerInterval: 500
44});
45
46export async function middleware(request: NextRequest) {
47 // Użyj IP jako identyfikatora
48 const ip = request.ip ?? 'anonymous';
49
50 try {
51 const { success, remaining } = limiter.check(10, ip); // 10 requestów na minutę
52
53 if (!success) {
54 return new NextResponse('Too Many Requests', {
55 status: 429,
56 headers: {
57 'X-RateLimit-Limit': '10',
58 'X-RateLimit-Remaining': '0',
59 'X-RateLimit-Reset': new Date(Date.now() + 60000).toISOString()
60 }
61 });
62 }
63
64 const response = NextResponse.next();
65 response.headers.set('X-RateLimit-Limit', '10');
66 response.headers.set('X-RateLimit-Remaining', remaining.toString());
67
68 return response;
69 } catch (error) {
70 return new NextResponse('Internal Server Error', { status: 500 });
71 }
72}
73
74export const config = {
75 matcher: '/api/:path*'
76};1// lib/rate-limit-server-action.ts
2import { cookies } from 'next/headers';
3
4const rateLimitStore = new Map<string, { count: number; resetAt: number }>();
5
6export async function checkRateLimit(
7 action: string,
8 limit: number = 10,
9 windowMs: number = 60000
10): Promise<boolean> {
11 const cookieStore = await cookies();
12 const sessionId = cookieStore.get('session-id')?.value || 'anonymous';
13 const key = `${sessionId}:${action}`;
14
15 const now = Date.now();
16 const record = rateLimitStore.get(key);
17
18 if (!record || now > record.resetAt) {
19 rateLimitStore.set(key, {
20 count: 1,
21 resetAt: now + windowMs
22 });
23 return true;
24 }
25
26 if (record.count >= limit) {
27 return false;
28 }
29
30 record.count++;
31 return true;
32}
33
34// app/actions.ts
35'use server';
36
37import { checkRateLimit } from '@/lib/rate-limit-server-action';
38
39export async function sensitiveAction(data: FormData) {
40 const allowed = await checkRateLimit('sensitive-action', 5, 60000);
41
42 if (!allowed) {
43 throw new Error('Rate limit exceeded. Please try again later.');
44 }
45
46 // Wykonaj akcję...
47}Odpowiednie nagłówki HTTP to fundament bezpieczeństwa aplikacji webowych:
1// next.config.js
2const securityHeaders = [
3 {
4 key: 'X-DNS-Prefetch-Control',
5 value: 'on'
6 },
7 {
8 key: 'Strict-Transport-Security',
9 value: 'max-age=63072000; includeSubDomains; preload'
10 },
11 {
12 key: 'X-Frame-Options',
13 value: 'SAMEORIGIN'
14 },
15 {
16 key: 'X-Content-Type-Options',
17 value: 'nosniff'
18 },
19 {
20 key: 'X-XSS-Protection',
21 value: '1; mode=block'
22 },
23 {
24 key: 'Referrer-Policy',
25 value: 'strict-origin-when-cross-origin'
26 },
27 {
28 key: 'Permissions-Policy',
29 value: 'camera=(), microphone=(), geolocation=()'
30 }
31];
32
33module.exports = {
34 async headers() {
35 return [
36 {
37 source: '/:path*',
38 headers: securityHeaders
39 }
40 ];
41 }
42};Lub przez middleware dla większej kontroli:
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 // HSTS - wymusza HTTPS
9 response.headers.set(
10 'Strict-Transport-Security',
11 'max-age=31536000; includeSubDomains'
12 );
13
14 // X-Frame-Options - ochrona przed clickjacking
15 response.headers.set('X-Frame-Options', 'SAMEORIGIN');
16
17 // X-Content-Type-Options - zapobiega MIME sniffing
18 response.headers.set('X-Content-Type-Options', 'nosniff');
19
20 // X-XSS-Protection - dodatkowa ochrona przed XSS
21 response.headers.set('X-XSS-Protection', '1; mode=block');
22
23 // Referrer-Policy - kontrola informacji w nagłówku Referer
24 response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
25
26 // Permissions-Policy - kontrola dostępu do API przeglądarki
27 response.headers.set(
28 'Permissions-Policy',
29 'camera=(), microphone=(), geolocation=(), payment=()'
30 );
31
32 return response;
33}1// app/actions.ts
2'use server';
3
4import { z } from 'zod';
5import { revalidatePath } from 'next/cache';
6
7const CreatePostSchema = z.object({
8 title: z.string().min(3).max(100),
9 content: z.string().min(10).max(5000),
10 tags: z.array(z.string()).max(5).optional()
11});
12
13export async function createPost(formData: FormData) {
14 // Walidacja danych wejściowych
15 const rawData = {
16 title: formData.get('title'),
17 content: formData.get('content'),
18 tags: formData.getAll('tags')
19 };
20
21 const validation = CreatePostSchema.safeParse(rawData);
22
23 if (!validation.success) {
24 return {
25 error: 'Validation failed',
26 issues: validation.error.issues
27 };
28 }
29
30 const { title, content, tags } = validation.data;
31
32 // Weryfikacja autentykacji
33 const session = await getServerSession();
34 if (!session?.user) {
35 return { error: 'Unauthorized' };
36 }
37
38 // Sanityzacja HTML (jeśli pozwalamy na formatowanie)
39 const sanitizedContent = sanitizeHtml(content);
40
41 try {
42 const post = await db.post.create({
43 data: {
44 title,
45 content: sanitizedContent,
46 tags,
47 authorId: session.user.id
48 }
49 });
50
51 revalidatePath('/blog');
52
53 return { success: true, post };
54 } catch (error) {
55 console.error('Failed to create post:', error);
56 return { error: 'Failed to create post' };
57 }
58}1// app/api/users/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { getServerSession } from 'next-auth';
4import { z } from 'zod';
5
6const QuerySchema = z.object({
7 page: z.coerce.number().min(1).default(1),
8 limit: z.coerce.number().min(1).max(100).default(10)
9});
10
11export async function GET(request: NextRequest) {
12 try {
13 // Weryfikacja autentykacji
14 const session = await getServerSession();
15 if (!session) {
16 return NextResponse.json(
17 { error: 'Unauthorized' },
18 { status: 401 }
19 );
20 }
21
22 // Walidacja query parameters
23 const { searchParams } = new URL(request.url);
24 const validation = QuerySchema.safeParse({
25 page: searchParams.get('page'),
26 limit: searchParams.get('limit')
27 });
28
29 if (!validation.success) {
30 return NextResponse.json(
31 { error: 'Invalid parameters', issues: validation.error.issues },
32 { status: 400 }
33 );
34 }
35
36 const { page, limit } = validation.data;
37
38 // Bezpieczne query do bazy danych
39 const users = await db.user.findMany({
40 skip: (page - 1) * limit,
41 take: limit,
42 select: {
43 id: true,
44 name: true,
45 email: true,
46 // NIE zwracaj hasła ani innych wrażliwych danych
47 }
48 });
49
50 return NextResponse.json({ users });
51 } catch (error) {
52 console.error('API Error:', error);
53 // NIE zwracaj szczegółów błędów w produkcji
54 return NextResponse.json(
55 { error: 'Internal server error' },
56 { status: 500 }
57 );
58 }
59}1// .env.local (NIGDY nie commituj do repo!)
2DATABASE_URL="postgresql://..."
3NEXTAUTH_SECRET="super-secret-key"
4API_KEY="secret-api-key"
5
6// app/config.ts
7const requiredEnvVars = [
8 'DATABASE_URL',
9 'NEXTAUTH_SECRET',
10 'API_KEY'
11] as const;
12
13// Walidacja obecności zmiennych środowiskowych przy starcie
14requiredEnvVars.forEach((varName) => {
15 if (!process.env[varName]) {
16 throw new Error(`Missing required environment variable: ${varName}`);
17 }
18});
19
20export const config = {
21 database: {
22 url: process.env.DATABASE_URL!
23 },
24 auth: {
25 secret: process.env.NEXTAUTH_SECRET!
26 },
27 api: {
28 key: process.env.API_KEY!
29 }
30} as const;1// lib/cookies.ts
2import { cookies } from 'next/headers';
3
4export async function setSecureCookie(name: string, value: string) {
5 const cookieStore = await cookies();
6
7 cookieStore.set(name, value, {
8 httpOnly: true, // Niedostępne dla JavaScript
9 secure: process.env.NODE_ENV === 'production', // Tylko HTTPS w produkcji
10 sameSite: 'strict', // Ochrona przed CSRF
11 maxAge: 60 * 60 * 24 * 7, // 7 dni
12 path: '/'
13 });
14}1// lib/logger.ts
2export function logSecurityEvent(event: {
3 type: 'auth_failure' | 'rate_limit' | 'csrf_violation' | 'xss_attempt';
4 ip: string;
5 userAgent: string;
6 details?: any;
7}) {
8 const logEntry = {
9 timestamp: new Date().toISOString(),
10 ...event
11 };
12
13 // W produkcji wyślij do systemu monitoringu (Sentry, LogRocket, etc.)
14 if (process.env.NODE_ENV === 'production') {
15 // sendToMonitoring(logEntry);
16 }
17
18 console.warn('Security Event:', logEntry);
19}
20
21// middleware.ts
22export function middleware(request: NextRequest) {
23 const ip = request.ip ?? 'unknown';
24 const userAgent = request.headers.get('user-agent') ?? 'unknown';
25
26 // Loguj podejrzane aktywności
27 if (isSuspicious(request)) {
28 logSecurityEvent({
29 type: 'csrf_violation',
30 ip,
31 userAgent,
32 details: { url: request.url }
33 });
34 }
35
36 return NextResponse.next();
37}W Metropolii Quantum bezpieczeństwo to proces ciągły, nie jednorazowa akcja. Kluczowe zasady:
Podsumowując, oto kluczowe zasady optymalizacji wydajności w Next.js:
Image z Next.jsSuspense dla lepszej responsywności UIStosując te praktyki, możesz tworzyć aplikacje Next.js, które są zarówno funkcjonalne, jak i błyskawicznie szybkie, zapewniając użytkownikom doskonałe doświadczenia niezależnie od ich lokalizacji czy używanego urządzenia.