Next.js Pytania Rekrutacyjne - Kompletny Przewodnik 2026

Sławomir Plamowski 51 min czytania
frontend nextjs pytania-rekrutacyjne react rozmowa-rekrutacyjna ssr

Next.js w 2026 to nie jest "React z SSR". To pełnoprawny framework fullstack z Server Components, App Router, Server Actions i Partial Prerendering. Jeśli rekrutujesz na stanowisko frontend developer i w ofercie jest Next.js - musisz znać te koncepcje na wylot.

W tym przewodniku znajdziesz 40 najczęstszych pytań rekrutacyjnych z Next.js - od podstaw App Router po zaawansowane Server Actions i optymalizację. Każda odpowiedź w formacie "30 sekund / 2 minuty" - dokładnie tak, jak na prawdziwej rozmowie.

Spis Treści

  1. Podstawy i Architektura
  2. Routing w App Router
  3. Renderowanie: SSR, SSG, ISR
  4. Pobieranie Danych
  5. Optymalizacja i Wydajność
  6. Metadata i SEO
  7. API i Backend
  8. Konfiguracja i Deployment

Podstawy i Architektura

1. Czym jest Next.js i jakie problemy rozwiązuje w porównaniu do czystego React?

Odpowiedź w 30 sekund:

Next.js to framework React oferujący Server Components, automatyczne SSR/SSG/ISR, routing oparty na plikach, optymalizację obrazów i fontów. Rozwiązuje problemy, które w czystym React wymagają ręcznej konfiguracji: SEO (SSR), wydajność (code splitting), routing (React Router), i konfigurację buildu.

Odpowiedź w 2 minuty:

Next.js rozwiązuje kilka kluczowych problemów czystego React:

SEO i First Contentful Paint:

// Czysty React - klient renderuje pustą stronę, potem JS ładuje content
// Google może nie zobaczyć treści

// Next.js - serwer wysyła gotowy HTML
// app/products/page.tsx
export default async function ProductsPage() {
  const products = await getProducts(); // Wykonuje się na serwerze
  return (
    <ul>
      {products.map(p => <li key={p.id}>{p.name}</li>)}
    </ul>
  );
}
// Google widzi pełną treść od razu

Routing bez dodatkowych bibliotek:

app/
├── page.tsx           # /
├── about/page.tsx     # /about
├── products/
│   ├── page.tsx       # /products
│   └── [id]/page.tsx  # /products/123

Optymalizacje out-of-the-box:

  • Automatyczny code splitting per route
  • Image optimization z next/image
  • Font optimization z next/font
  • Prefetching linków

Server Components:

  • Komponenty renderowane na serwerze, zero JS do klienta
  • Bezpośredni dostęp do bazy danych bez API
  • Mniejszy bundle size

2. Jakie są różnice między App Router a Pages Router w Next.js?

Odpowiedź w 30 sekund:

App Router (od Next.js 13) używa React Server Components jako domyślnych, oferuje layouts, loading states, error boundaries i Server Actions. Pages Router to starsze API z getServerSideProps/getStaticProps. App Router jest rekomendowany dla nowych projektów.

Odpowiedź w 2 minuty:

Główne różnice między Pages Router a App Router obejmują architekturę komponentów, sposób pobierania danych oraz obsługę stanów aplikacji - poniższa tabela pokazuje porównanie:

Cecha Pages Router App Router
Lokalizacja /pages /app
Komponenty Wszystkie Client Server Components domyślnie
Data fetching getServerSideProps, getStaticProps async komponenty, fetch
Layouts _app.tsx, _document.tsx Zagnieżdżone layout.tsx
Loading states Ręczne loading.tsx
Error handling Ręczne error.tsx
Metadata Head component Metadata API

Pages Router (stare API):

// pages/products/[id].tsx
export async function getServerSideProps({ params }) {
  const product = await getProduct(params.id);
  return { props: { product } };
}

export default function ProductPage({ product }) {
  return <div>{product.name}</div>;
}

App Router (nowe API):

// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id); // Bezpośrednio w komponencie!
  return <div>{product.name}</div>;
}

App Router oferuje też:

  • Streaming z Suspense
  • Parallel Routes (@modal, @sidebar)
  • Intercepting Routes ((.)photo, (..)settings)
  • Server Actions bez API routes

3. Czym są Server Components i Client Components w Next.js 13+?

Odpowiedź w 30 sekund:

Server Components renderują się na serwerze, nie wysyłają JS do klienta i mogą bezpośrednio czytać z bazy danych. Client Components (z dyrektywą 'use client') działają w przeglądarce i obsługują interakcje, hooks useState/useEffect, eventy. W App Router komponenty są Server Components domyślnie.

Odpowiedź w 2 minuty:

Server Components (domyślne w App Router):

// app/products/page.tsx - Server Component
import { db } from '@/lib/db';

export default async function ProductsPage() {
  // Bezpośredni dostęp do bazy - bez API!
  const products = await db.product.findMany();

  return (
    <ul>
      {products.map(p => (
        <ProductCard key={p.id} product={p} />
      ))}
    </ul>
  );
}
// Ten komponent NIE trafia do bundle JS klienta

Client Components:

// components/AddToCartButton.tsx
'use client'; // Dyrektywa wymagana!

import { useState } from 'react';

export function AddToCartButton({ productId }) {
  const [loading, setLoading] = useState(false);

  const handleClick = async () => {
    setLoading(true);
    await addToCart(productId);
    setLoading(false);
  };

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Dodawanie...' : 'Dodaj do koszyka'}
    </button>
  );
}

Kiedy używać którego:

Server Component Client Component
Pobieranie danych useState, useEffect
Dostęp do backendu Event handlers (onClick)
Wrażliwe dane (API keys) Browser APIs (localStorage)
Duże zależności (nie idą do bundle) Interaktywne UI

Kompozycja - Server owija Client:

// app/products/[id]/page.tsx (Server)
export default async function ProductPage({ params }) {
  const product = await getProduct(params.id);

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      {/* Client Component wewnątrz Server Component */}
      <AddToCartButton productId={product.id} />
    </div>
  );
}

4. Kiedy używać dyrektywy "use client" a kiedy "use server"?

Odpowiedź w 30 sekund:

'use client' oznacza komponent kliencki z interaktywnością (hooks, eventy). 'use server' definiuje Server Actions - funkcje wywoływane z klienta, wykonywane na serwerze (mutacje danych, operacje DB). Domyślnie komponenty w App Router są Server Components - nie wymagają dyrektywy.

Odpowiedź w 2 minuty:

'use client' - dla interaktywności:

'use client';

import { useState, useEffect } from 'react';

export function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Mounted in browser');
  }, []);

  return (
    <button onClick={() => setCount(c => c + 1)}>
      Count: {count}
    </button>
  );
}

'use server' - dla Server Actions:

// app/actions.ts
'use server';

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title');

  await db.post.create({ data: { title } });

  revalidatePath('/posts'); // Odśwież cache
}

Użycie Server Action w formularzu:

// app/posts/new/page.tsx (Server Component)
import { createPost } from '../actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <button type="submit">Utwórz</button>
    </form>
  );
}
// Działa nawet bez JavaScript! (Progressive Enhancement)

Zasada: Nie dodawaj 'use client' "na wszelki wypadek" - tracisz korzyści Server Components (mniejszy bundle, bezpośredni dostęp do danych).


5. Jak działa hydracja w Next.js i jakie są jej implikacje dla wydajności?

Odpowiedź w 30 sekund:

Hydracja to proces, gdzie React "przejmuje" statyczny HTML wysłany przez serwer i dodaje interaktywność (event handlers, state). W Next.js Server Components nie wymagają hydracji (zero JS), tylko Client Components. To znacząco zmniejsza czas do interaktywności (TTI).

Odpowiedź w 2 minuty:

Proces hydracji:

  1. Serwer renderuje HTML i wysyła do przeglądarki
  2. Przeglądarka wyświetla HTML (użytkownik widzi content)
  3. JavaScript się ładuje
  4. React "hydruje" - łączy HTML z event handlers
  5. Strona staje się interaktywna

Problem z tradycyjnym SSR:

Serwer: renderuje wszystko → HTML
Klient: ładuje CAŁY JS → hydruje WSZYSTKO
         ↓
Długi Time to Interactive (TTI)

Next.js z Server Components:

Server Components: renderują na serwerze → HTML (zero JS)
Client Components: tylko te wymagające interaktywności → JS + hydracja
         ↓
Mniejszy bundle, szybszy TTI

Streaming i Suspense:

// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* Natychmiast pokazuje header */}

      <Suspense fallback={<p>Ładowanie statystyk...</p>}>
        <SlowStats /> {/* Streamuje gdy gotowe */}
      </Suspense>

      <Suspense fallback={<p>Ładowanie wykresu...</p>}>
        <SlowChart /> {/* Niezależnie streamuje */}
      </Suspense>
    </div>
  );
}

Implikacje:

  • Unikaj "use client" na wysokim poziomie drzewa
  • Client Components głęboko w hierarchii = mniej JS do hydracji
  • Selective Hydration - React hydruje najpierw to, z czym użytkownik wchodzi w interakcję

Routing w App Router

6. Jak działa system routingu oparty na plikach w App Router?

Odpowiedź w 30 sekund:

W App Router struktura folderów w /app definiuje routing. Folder = segment URL, page.tsx = renderowalna strona, layout.tsx = współdzielony layout. Specjalne pliki: loading.tsx, error.tsx, not-found.tsx obsługują stany UI automatycznie.

Odpowiedź w 2 minuty:

Struktura folderów = URL:

app/
├── page.tsx              # /
├── layout.tsx            # Root layout (wymagany)
├── loading.tsx           # Loading UI dla /
├── error.tsx             # Error UI dla /
├── not-found.tsx         # 404 dla /
│
├── about/
│   └── page.tsx          # /about
│
├── blog/
│   ├── page.tsx          # /blog
│   ├── layout.tsx        # Layout dla /blog/*
│   └── [slug]/
│       └── page.tsx      # /blog/my-post
│
├── products/
│   ├── page.tsx          # /products
│   └── [id]/
│       ├── page.tsx      # /products/123
│       └── reviews/
│           └── page.tsx  # /products/123/reviews

Specjalne pliki:

Plik Funkcja
page.tsx UI strony (wymagany do renderowania)
layout.tsx Współdzielony layout (nie re-renderuje przy nawigacji)
loading.tsx Loading state (Suspense boundary)
error.tsx Error boundary
not-found.tsx 404 page
route.ts API endpoint (Route Handler)

Przykład page.tsx:

// app/products/[id]/page.tsx
export default async function ProductPage({
  params
}: {
  params: { id: string }
}) {
  const product = await getProduct(params.id);

  if (!product) {
    notFound(); // Pokazuje not-found.tsx
  }

  return <div>{product.name}</div>;
}

7. Czym są segmenty dynamiczne i jak je definiować w Next.js?

Odpowiedź w 30 sekund:

Segmenty dynamiczne to foldery w nawiasach kwadratowych: [id], [slug]. Wartość jest przekazywana w params. Warianty: [...slug] (catch-all: /a/b/c), [[...slug]] (optional catch-all: również /).

Odpowiedź w 2 minuty:

Podstawowy segment dynamiczny:

app/products/[id]/page.tsx → /products/123, /products/abc
// app/products/[id]/page.tsx
export default function ProductPage({
  params
}: {
  params: { id: string }
}) {
  return <div>Product ID: {params.id}</div>;
}

Catch-all segments [...slug]:

app/docs/[...slug]/page.tsx → /docs/a, /docs/a/b, /docs/a/b/c
// app/docs/[...slug]/page.tsx
export default function DocsPage({
  params
}: {
  params: { slug: string[] }
}) {
  // /docs/getting-started/installation
  // params.slug = ['getting-started', 'installation']
  return <div>Path: {params.slug.join('/')}</div>;
}

Optional catch-all [[...slug]]:

app/shop/[[...slug]]/page.tsx → /shop, /shop/clothes, /shop/clothes/shirts
// app/shop/[[...slug]]/page.tsx
export default function ShopPage({
  params
}: {
  params: { slug?: string[] }
}) {
  // /shop → params.slug = undefined
  // /shop/clothes → params.slug = ['clothes']

  if (!params.slug) {
    return <div>All products</div>;
  }

  return <div>Category: {params.slug.join(' > ')}</div>;
}

Generowanie statycznych ścieżek:

// app/products/[id]/page.tsx
export async function generateStaticParams() {
  const products = await getProducts();

  return products.map((product) => ({
    id: product.id.toString(),
  }));
}
// Pre-renderuje /products/1, /products/2, etc.

8. Jak zaimplementować zagnieżdżone layouty w App Router?

Odpowiedź w 30 sekund:

Każdy folder może mieć layout.tsx, który owija page.tsx i zagnieżdżone layouty. Layouty nie re-renderują się przy nawigacji między stronami - zachowują stan. Root layout (app/layout.tsx) jest wymagany i zawiera <html> i <body>.

Odpowiedź w 2 minuty:

Struktura zagnieżdżonych layoutów:

app/
├── layout.tsx          # Root Layout (html, body)
├── page.tsx            # /
│
├── dashboard/
│   ├── layout.tsx      # Dashboard Layout (sidebar)
│   ├── page.tsx        # /dashboard
│   │
│   ├── settings/
│   │   ├── layout.tsx  # Settings Layout (tabs)
│   │   └── page.tsx    # /dashboard/settings
│   │
│   └── analytics/
│       └── page.tsx    # /dashboard/analytics

Root Layout (wymagany):

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="pl">
      <body>
        <Header />
        <main>{children}</main>
        <Footer />
      </body>
    </html>
  );
}

Dashboard Layout:

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="flex">
      <Sidebar /> {/* Nie re-renderuje przy nawigacji! */}
      <div className="flex-1">{children}</div>
    </div>
  );
}

Settings Layout z tabami:

// app/dashboard/settings/layout.tsx
export default function SettingsLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <nav>
        <Link href="/dashboard/settings">General</Link>
        <Link href="/dashboard/settings/security">Security</Link>
      </nav>
      {children}
    </div>
  );
}

Wynik dla /dashboard/settings:

RootLayout
└── DashboardLayout (sidebar)
    └── SettingsLayout (tabs)
        └── SettingsPage

9. Jakie jest zastosowanie plików loading.tsx i error.tsx?

Odpowiedź w 30 sekund:

loading.tsx automatycznie owija page.tsx w React Suspense boundary - pokazuje się podczas ładowania async komponentów. error.tsx to Error Boundary - łapie błędy w segmencie i pozwala na recovery bez crashowania całej aplikacji.

Odpowiedź w 2 minuty:

loading.tsx - automatyczny Suspense:

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
      <div className="h-64 bg-gray-200 rounded"></div>
    </div>
  );
}

To jest równoważne:

<Suspense fallback={<Loading />}>
  <DashboardPage />
</Suspense>

error.tsx - Error Boundary:

// app/dashboard/error.tsx
'use client'; // Error boundaries muszą być Client Components!

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div className="p-4 bg-red-50 rounded">
      <h2>Coś poszło nie tak!</h2>
      <p>{error.message}</p>
      <button
        onClick={() => reset()}
        className="mt-4 px-4 py-2 bg-red-500 text-white rounded"
      >
        Spróbuj ponownie
      </button>
    </div>
  );
}

Hierarchia błędów:

app/
├── error.tsx           # Łapie błędy z całej aplikacji
├── dashboard/
│   ├── error.tsx       # Łapie błędy tylko z /dashboard/*
│   └── page.tsx        # Jeśli rzuci błąd → dashboard/error.tsx

global-error.tsx dla Root Layout:

// app/global-error.tsx
'use client';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <h2>Krytyczny błąd!</h2>
        <button onClick={() => reset()}>Odśwież</button>
      </body>
    </html>
  );
}

10. Czym są grupy tras (route groups) i do czego służą?

Odpowiedź w 30 sekund:

Route groups to foldery w nawiasach (nazwa), które organizują pliki bez wpływu na URL. Służą do: współdzielenia layoutów między niezwiązanymi stronami, organizacji kodu, tworzenia wielu root layoutów dla różnych sekcji aplikacji.

Odpowiedź w 2 minuty:

Organizacja bez wpływu na URL:

app/
├── (marketing)/           # Nie wpływa na URL
│   ├── layout.tsx         # Layout dla marketing pages
│   ├── page.tsx           # / (strona główna)
│   ├── about/page.tsx     # /about
│   └── pricing/page.tsx   # /pricing
│
├── (shop)/                # Osobny layout
│   ├── layout.tsx         # Layout dla shop (koszyk, etc.)
│   ├── products/page.tsx  # /products
│   └── cart/page.tsx      # /cart
│
├── (auth)/                # Layout bez nawigacji
│   ├── layout.tsx         # Minimal layout
│   ├── login/page.tsx     # /login
│   └── register/page.tsx  # /register

Różne layouty dla różnych sekcji:

// app/(marketing)/layout.tsx
export default function MarketingLayout({ children }) {
  return (
    <div>
      <MarketingHeader /> {/* Z CTA "Sign Up" */}
      {children}
      <MarketingFooter />
    </div>
  );
}

// app/(shop)/layout.tsx
export default function ShopLayout({ children }) {
  return (
    <div>
      <ShopHeader /> {/* Z koszykiem */}
      {children}
      <CartSidebar />
    </div>
  );
}

Wiele root layoutów:

app/
├── (app)/
│   ├── layout.tsx      # Layout dla zalogowanych
│   └── dashboard/
│       └── page.tsx
│
├── (public)/
│   ├── layout.tsx      # Layout publiczny
│   └── page.tsx

11. Jak zaimplementować middleware w Next.js i jakie ma zastosowania?

Odpowiedź w 30 sekund:

Middleware w Next.js to funkcja w middleware.ts w root projektu, wykonywana przed każdym requestem. Zastosowania: autoryzacja, redirecty, rewrite URLs, A/B testing, geolokacja, rate limiting. Działa na Edge Runtime - bardzo szybko.

Odpowiedź w 2 minuty:

Podstawowy middleware:

// middleware.ts (w root projektu)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Logika przed każdym requestem
  console.log('Path:', request.nextUrl.pathname);

  return NextResponse.next();
}

// Opcjonalnie: określ które ścieżki
export const config = {
  matcher: ['/dashboard/:path*', '/api/:path*'],
};

Autoryzacja:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const token = request.cookies.get('session')?.value;

  // Chronione ścieżki
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!token) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  // Zalogowani nie widzą /login
  if (request.nextUrl.pathname === '/login' && token) {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return NextResponse.next();
}

Geolokacja i A/B testing:

export function middleware(request: NextRequest) {
  const country = request.geo?.country || 'US';

  // Redirect na lokalną wersję
  if (country === 'PL' && !request.nextUrl.pathname.startsWith('/pl')) {
    return NextResponse.redirect(new URL(`/pl${request.nextUrl.pathname}`, request.url));
  }

  // A/B testing
  const bucket = request.cookies.get('ab-bucket')?.value ||
    (Math.random() > 0.5 ? 'a' : 'b');

  const response = NextResponse.next();
  response.cookies.set('ab-bucket', bucket);

  return response;
}

Matcher patterns:

export const config = {
  matcher: [
    // Match all paths except static files
    '/((?!_next/static|_next/image|favicon.ico).*)',
    // Or specific paths
    '/dashboard/:path*',
    '/api/:path*',
  ],
};

12. Czym są parallel routes i intercepting routes?

Odpowiedź w 30 sekund:

Parallel routes (@folder) renderują wiele stron jednocześnie w tym samym layoucie - np. dashboard z wieloma panelami. Intercepting routes ((.), (..)) przechwytują nawigację i pokazują inny content - np. modal ze zdjęciem zamiast pełnej strony.

Odpowiedź w 2 minuty:

Parallel Routes (@nazwa):

app/
├── layout.tsx
├── @dashboard/
│   └── page.tsx         # Slot "dashboard"
├── @analytics/
│   └── page.tsx         # Slot "analytics"
└── page.tsx
// app/layout.tsx
export default function Layout({
  children,
  dashboard,
  analytics,
}: {
  children: React.ReactNode;
  dashboard: React.ReactNode;
  analytics: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-2">
      <div>{dashboard}</div>
      <div>{analytics}</div>
    </div>
  );
}

Intercepting Routes - Modal pattern:

app/
├── @modal/
│   └── (.)photos/[id]/
│       └── page.tsx     # Intercepted route (modal)
├── photos/
│   └── [id]/
│       └── page.tsx     # Prawdziwa strona (full page)
├── layout.tsx
└── page.tsx
// app/layout.tsx
export default function Layout({
  children,
  modal,
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <>
      {children}
      {modal} {/* Modal wyświetla się nad contentem */}
    </>
  );
}

// app/@modal/(.)photos/[id]/page.tsx
export default function PhotoModal({ params }) {
  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center">
      <div className="bg-white p-4 rounded">
        <Image src={`/photos/${params.id}.jpg`} />
      </div>
    </div>
  );
}

Intercepting patterns:

  • (.) - ten sam poziom
  • (..) - jeden poziom wyżej
  • (..)(..) - dwa poziomy wyżej
  • (...) - od root

Renderowanie: SSR, SSG, ISR

13. Jakie strategie renderowania oferuje Next.js (SSR, SSG, ISR)?

Odpowiedź w 30 sekund:

SSG (Static Site Generation) - generuje HTML w build time. SSR (Server-Side Rendering) - generuje HTML na każdy request. ISR (Incremental Static Regeneration) - statyczny z rewalidacją w tle. W App Router kontrolujesz to przez opcje fetch i export const.

Odpowiedź w 2 minuty:

Static Site Generation (SSG):

// Domyślne zachowanie dla stron bez dynamicznych danych
// app/about/page.tsx
export default function AboutPage() {
  return <div>O nas</div>;
}
// Generowane raz w build time

Server-Side Rendering (SSR):

// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'; // Wymusza SSR

export default async function DashboardPage() {
  const data = await fetchDashboardData();
  return <Dashboard data={data} />;
}
// Świeże dane na każdy request

Incremental Static Regeneration (ISR):

// app/products/page.tsx
export const revalidate = 60; // Rewalidacja co 60 sekund

export default async function ProductsPage() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 }, // Lub per-fetch
  });
  return <ProductList products={products} />;
}
// Statyczne, ale odświeża się w tle

Porównanie:

Strategia Build Time Request Time Kiedy używać
SSG HTML generowany Serwowany z cache Blog, dokumentacja
SSR - HTML generowany Personalizacja, real-time
ISR HTML generowany Cache + rewalidacja E-commerce, news

Kontrola w App Router:

// Wymusz SSR
export const dynamic = 'force-dynamic';

// Wymusz SSG
export const dynamic = 'force-static';

// Rewalidacja czasowa
export const revalidate = 3600; // 1 godzina

// Brak cache
export const revalidate = 0;

14. Czym jest Incremental Static Regeneration (ISR) i jak go skonfigurować?

Odpowiedź w 30 sekund:

ISR pozwala regenerować statyczne strony bez pełnego rebuildu. Ustawiasz revalidate w sekundach - strona jest statyczna, ale po upływie czasu Next.js regeneruje ją w tle przy następnym requeście. Łączy szybkość SSG z aktualnością danych.

Odpowiedź w 2 minuty:

Jak działa ISR:

  1. Pierwszy request → serwuje statyczny HTML z build time
  2. Po upływie revalidate → następny request triggeruje regenerację w tle
  3. Stary HTML serwowany dopóki nowy nie będzie gotowy
  4. Nowy HTML zastępuje stary w cache

Konfiguracja na poziomie strony:

// app/products/page.tsx
export const revalidate = 60; // Rewaliduj co 60 sekund

export default async function ProductsPage() {
  const products = await getProducts();
  return <ProductList products={products} />;
}

Konfiguracja per-fetch:

// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
  // Ten fetch rewaliduje co 60 sekund
  const product = await fetch(`https://api.example.com/products/${params.id}`, {
    next: { revalidate: 60 },
  });

  // Ten fetch jest świeży na każdy request
  const stock = await fetch(`https://api.example.com/stock/${params.id}`, {
    cache: 'no-store',
  });

  return <Product product={product} stock={stock} />;
}

On-demand revalidation (rewalidacja na żądanie):

// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';

export async function POST(request: Request) {
  const { path, tag, secret } = await request.json();

  if (secret !== process.env.REVALIDATION_SECRET) {
    return Response.json({ error: 'Invalid secret' }, { status: 401 });
  }

  if (path) {
    revalidatePath(path); // Rewaliduj konkretną ścieżkę
  }

  if (tag) {
    revalidateTag(tag); // Rewaliduj wszystkie z tym tagiem
  }

  return Response.json({ revalidated: true });
}

Tagowanie dla grupowej rewalidacji:

// Fetch z tagiem
const products = await fetch('https://api.example.com/products', {
  next: { tags: ['products'] },
});

// Rewaliduj wszystkie z tagiem 'products'
revalidateTag('products');

15. Jak działa streaming i Suspense w Next.js?

Odpowiedź w 30 sekund:

Streaming pozwala wysyłać HTML w kawałkach zamiast czekać na całą stronę. Next.js używa React Suspense - owija wolne komponenty w <Suspense> z fallbackiem. Serwer streamuje gotowe części, użytkownik widzi content wcześniej.

Odpowiedź w 2 minuty:

Tradycyjny SSR vs Streaming:

Tradycyjny SSR:
1. Fetch ALL data
2. Render ALL HTML
3. Send ALL HTML      ← Użytkownik czeka
4. Load ALL JS
5. Hydrate ALL

Streaming:
1. Send shell immediately  ← Użytkownik widzi layout
2. Stream components as ready
3. Hydrate incrementally

Implementacja z Suspense:

// app/dashboard/page.tsx
import { Suspense } from 'react';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* Natychmiast widoczne */}

      <Suspense fallback={<StatsSkeleton />}>
        <SlowStats />  {/* Streamowane gdy gotowe */}
      </Suspense>

      <Suspense fallback={<ChartSkeleton />}>
        <SlowChart />  {/* Niezależnie streamowane */}
      </Suspense>

      <Suspense fallback={<TableSkeleton />}>
        <SlowDataTable />  {/* Niezależnie streamowane */}
      </Suspense>
    </div>
  );
}

// Wolny komponent - async Server Component
async function SlowStats() {
  const stats = await fetch('https://api.example.com/stats'); // 3 sekundy
  return <StatsDisplay stats={stats} />;
}

loading.tsx jako automatyczny Suspense:

// app/dashboard/loading.tsx
export default function Loading() {
  return <DashboardSkeleton />;
}

// Automatycznie owija page.tsx w Suspense
// Równoważne:
<Suspense fallback={<Loading />}>
  <DashboardPage />
</Suspense>

Nested Suspense dla granularnej kontroli:

export default async function ProductPage({ params }) {
  const product = await getProduct(params.id); // Szybkie

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>

      {/* Recenzje mogą być wolne - nie blokują produktu */}
      <Suspense fallback={<p>Ładowanie recenzji...</p>}>
        <Reviews productId={params.id} />
      </Suspense>

      {/* Rekomendacje jeszcze wolniejsze */}
      <Suspense fallback={<p>Ładowanie rekomendacji...</p>}>
        <Recommendations productId={params.id} />
      </Suspense>
    </div>
  );
}

16. Kiedy wybrać Static Generation a kiedy Server-side Rendering?

Odpowiedź w 30 sekund:

SSG gdy dane nie zmieniają się między requestami (blog, docs, marketing). SSR gdy potrzebujesz danych w czasie rzeczywistym lub personalizacji (dashboard, koszyk). ISR to kompromis - statyczne z okresową aktualizacją (e-commerce, news).

Odpowiedź w 2 minuty:

Decision tree:

Czy dane są takie same dla wszystkich użytkowników?
│
├── TAK → Czy często się zmieniają?
│         │
│         ├── NIE → SSG (blog, docs, landing pages)
│         │
│         └── TAK → Jak często?
│                   │
│                   ├── Co kilka minut/godzin → ISR
│                   │
│                   └── Real-time → SSR
│
└── NIE → SSR (personalizacja, auth-dependent content)

Przykłady zastosowań:

Typ strony Strategia Powód
Blog post SSG Content nie zmienia się
Dokumentacja SSG Zmienia się tylko przy deploy
Strona produktu ISR (60s) Ceny/stock mogą się zmieniać
Lista produktów ISR (300s) Nowe produkty co jakiś czas
Dashboard użytkownika SSR Personalizowane dane
Koszyk zakupowy SSR Dane sesji
Feed social media SSR Real-time
Strona główna e-commerce ISR (60s) Promocje, ale nie real-time

Implementacja hybrydowa:

// app/products/[id]/page.tsx
export default async function ProductPage({ params }) {
  // Statyczne dane produktu - ISR
  const product = await fetch(`/api/products/${params.id}`, {
    next: { revalidate: 3600 }, // 1 godzina
  });

  // Dynamiczny stan magazynowy - zawsze świeży
  const stock = await fetch(`/api/stock/${params.id}`, {
    cache: 'no-store',
  });

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <StockBadge count={stock.available} /> {/* Real-time */}
    </div>
  );
}

17. Czym jest Partial Prerendering (PPR) w Next.js 14+?

Odpowiedź w 30 sekund:

PPR łączy statyczny shell strony z dynamicznymi "dziurami". Statyczny HTML jest serwowany natychmiast z edge cache, dynamiczne części (w Suspense) są streamowane. Nie musisz wybierać między SSG a SSR - masz oba w jednej stronie.

Odpowiedź w 2 minuty:

Tradycyjne podejście:

Strona jest ALBO statyczna ALBO dynamiczna
└── Jeśli cokolwiek jest dynamiczne → cała strona SSR

PPR (Partial Prerendering):

Statyczny shell (instant z edge)
├── Header ← statyczny
├── Hero ← statyczny
├── [Suspense: PersonalizedContent] ← dynamiczny, streamowany
├── Products ← statyczny
├── [Suspense: CartWidget] ← dynamiczny, streamowany
└── Footer ← statyczny

Włączenie PPR (experimental w Next.js 14):

// next.config.js
module.exports = {
  experimental: {
    ppr: true,
  },
};

Przykład implementacji:

// app/page.tsx
import { Suspense } from 'react';

export default function HomePage() {
  return (
    <div>
      {/* Statyczny shell - prerendered */}
      <Header />
      <Hero />

      {/* Dynamiczny slot - streamowany */}
      <Suspense fallback={<WelcomeSkeleton />}>
        <PersonalizedWelcome /> {/* Wymaga auth/cookies */}
      </Suspense>

      {/* Statyczny content */}
      <FeaturedProducts />

      {/* Dynamiczny slot */}
      <Suspense fallback={<CartSkeleton />}>
        <CartPreview /> {/* Dane użytkownika */}
      </Suspense>

      <Footer />
    </div>
  );
}

// Ten komponent triggeruje dynamiczne renderowanie
async function PersonalizedWelcome() {
  const user = await getUser(); // Wymaga cookies
  return <h2>Witaj, {user.name}!</h2>;
}

Korzyści PPR:

  1. Instant TTFB - statyczny shell z edge cache
  2. Personalizacja - dynamiczne części bez blokowania
  3. SEO - statyczny content jest od razu widoczny
  4. DX - nie musisz wybierać SSG vs SSR per-page

Pobieranie Danych

18. Jak działa fetch w Server Components i czym różni się od fetch po stronie klienta?

Odpowiedź w 30 sekund:

W Server Components fetch jest rozszerzony o opcje cache Next.js. Domyślnie cachuje wyniki (jak SSG), możesz kontrolować przez cache: 'no-store' lub next: { revalidate }. Po stronie klienta fetch działa standardowo - bez automatycznego cache Next.js.

Odpowiedź w 2 minuty:

Server Component fetch (rozszerzony):

// app/products/page.tsx (Server Component)
export default async function ProductsPage() {
  // Domyślnie: cache: 'force-cache' (statyczny)
  const products = await fetch('https://api.example.com/products');

  // Wyłączenie cache (SSR)
  const stock = await fetch('https://api.example.com/stock', {
    cache: 'no-store',
  });

  // ISR - rewalidacja co 60s
  const featured = await fetch('https://api.example.com/featured', {
    next: { revalidate: 60 },
  });

  // Z tagiem do on-demand revalidation
  const categories = await fetch('https://api.example.com/categories', {
    next: { tags: ['categories'] },
  });

  return <ProductList products={products} />;
}

Client Component fetch (standardowy):

'use client';

import { useState, useEffect } from 'react';

export function ClientProducts() {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    // Standardowy fetch - bez Next.js cache
    fetch('/api/products')
      .then(res => res.json())
      .then(setProducts);
  }, []);

  return <ProductList products={products} />;
}

Deduplikacja requestów:

// Next.js automatycznie deduplikuje te same requesty w jednym renderze
async function ProductPage({ params }) {
  const product = await fetch(`/api/products/${params.id}`);
  return (
    <div>
      <ProductDetails product={product} />
      <RelatedProducts productId={params.id} />
    </div>
  );
}

async function RelatedProducts({ productId }) {
  // Ten sam URL - Next.js nie wykona drugiego requestu!
  const product = await fetch(`/api/products/${productId}`);
  return <Related categories={product.categories} />;
}

Różnice:

Cecha Server Component Client Component
Cache Automatyczny (Next.js) Brak (lub React Query)
Deduplikacja Automatyczna Ręczna
Wykonanie Build/request time Runtime (przeglądarka)
Secrets Bezpieczne Widoczne!

19. Jak skonfigurować cache i rewalidację danych w Next.js?

Odpowiedź w 30 sekund:

Cache kontrolujesz przez: cache: 'force-cache' (domyślne, statyczne), cache: 'no-store' (dynamiczne), next: { revalidate: seconds } (ISR). Rewalidacja on-demand przez revalidatePath() lub revalidateTag(). Możesz też ustawić domyślne zachowanie per-segment.

Odpowiedź w 2 minuty:

Opcje cache w fetch:

// Statyczne (domyślne)
fetch(url, { cache: 'force-cache' });

// Dynamiczne - zawsze świeże
fetch(url, { cache: 'no-store' });

// ISR - rewalidacja czasowa
fetch(url, { next: { revalidate: 60 } });

// Z tagiem
fetch(url, { next: { tags: ['products', 'featured'] } });

Konfiguracja per-segment:

// app/products/page.tsx

// Cała strona dynamiczna
export const dynamic = 'force-dynamic';

// Cała strona statyczna
export const dynamic = 'force-static';

// ISR dla całej strony
export const revalidate = 60;

// Brak cache
export const revalidate = 0;

On-demand revalidation w Server Action:

// app/actions.ts
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function updateProduct(id: string, data: FormData) {
  await db.product.update({ where: { id }, data });

  // Rewaliduj konkretną ścieżkę
  revalidatePath(`/products/${id}`);

  // Rewaliduj listing
  revalidatePath('/products');

  // Lub wszystkie z tagiem
  revalidateTag('products');
}

Revalidation via API route:

// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache';
import { NextRequest } from 'next/server';

export async function POST(request: NextRequest) {
  const tag = request.nextUrl.searchParams.get('tag');

  if (tag) {
    revalidateTag(tag);
    return Response.json({ revalidated: true, tag });
  }

  return Response.json({ error: 'Missing tag' }, { status: 400 });
}

// Webhook od CMS:
// POST /api/revalidate?tag=products

Cache layers w Next.js:

  1. Request Memoization - deduplikacja w jednym renderze
  2. Data Cache - persystentny cache fetch
  3. Full Route Cache - cache całych stron
  4. Router Cache - client-side cache nawigacji

20. Czym jest funkcja generateStaticParams i kiedy jej używać?

Odpowiedź w 30 sekund:

generateStaticParams generuje listę parametrów dla dynamicznych segmentów ([id], [slug]) w build time. Next.js pre-renderuje stronę dla każdego zestawu parametrów. Używaj dla SSG dynamicznych stron - blogi, produkty, dokumentacja.

Odpowiedź w 2 minuty:

Podstawowe użycie:

// app/blog/[slug]/page.tsx

// Generuje statyczne ścieżki w build time
export async function generateStaticParams() {
  const posts = await getPosts();

  return posts.map((post) => ({
    slug: post.slug,
  }));
}
// Wygeneruje: /blog/hello-world, /blog/next-js-guide, etc.

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  return <Article post={post} />;
}

Zagnieżdżone parametry:

// app/products/[category]/[id]/page.tsx

export async function generateStaticParams() {
  const products = await getProducts();

  return products.map((product) => ({
    category: product.category,
    id: product.id,
  }));
}
// Wygeneruje: /products/electronics/123, /products/books/456

Dynamiczne fallback:

// app/products/[id]/page.tsx

export async function generateStaticParams() {
  // Generuj tylko top 10 produktów w build time
  const topProducts = await getTopProducts(10);

  return topProducts.map((p) => ({ id: p.id }));
}

// dynamicParams kontroluje co się dzieje z nieznanymi ID
export const dynamicParams = true; // (domyślne) generuj on-demand
// export const dynamicParams = false; // 404 dla nieznanych

Z parent params:

// app/[locale]/blog/[slug]/page.tsx

export async function generateStaticParams({
  params: { locale },
}: {
  params: { locale: string };
}) {
  // Dostęp do parent params
  const posts = await getPostsByLocale(locale);

  return posts.map((post) => ({
    slug: post.slug,
  }));
}

Kiedy używać:

  • Blog posts z znaną listą
  • Strony produktów (top N w build, reszta on-demand)
  • Dokumentacja (wszystkie strony)
  • Multi-language sites

21. Jak obsługiwać równoległe zapytania o dane w Server Components?

Odpowiedź w 30 sekund:

Używaj Promise.all() dla niezależnych zapytań - wykonują się równolegle zamiast sekwencyjnie. Alternatywnie, przenieś fetch do osobnych komponentów i owij w Suspense - każdy streamuje niezależnie. Next.js automatycznie deduplikuje te same URL-e.

Odpowiedź w 2 minuty:

Problem: sekwencyjne zapytania (waterfall):

// ❌ Źle - każdy fetch czeka na poprzedni
export default async function Dashboard() {
  const user = await getUser();        // 200ms
  const posts = await getPosts();      // 300ms
  const comments = await getComments(); // 200ms
  // Łącznie: 700ms

  return <DashboardUI user={user} posts={posts} comments={comments} />;
}

Rozwiązanie 1: Promise.all()

// ✅ Dobrze - zapytania równolegle
export default async function Dashboard() {
  const [user, posts, comments] = await Promise.all([
    getUser(),      // 200ms
    getPosts(),     // 300ms  ← wszystkie startują równocześnie
    getComments(),  // 200ms
  ]);
  // Łącznie: 300ms (najdłuższy)

  return <DashboardUI user={user} posts={posts} comments={comments} />;
}

Rozwiązanie 2: Parallel fetching z Suspense (streaming):

// ✅ Najlepsze - streaming, każdy komponent niezależny
export default function Dashboard() {
  return (
    <div>
      <Suspense fallback={<UserSkeleton />}>
        <UserInfo />  {/* Fetch wewnątrz */}
      </Suspense>

      <Suspense fallback={<PostsSkeleton />}>
        <PostsList />  {/* Fetch wewnątrz */}
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <CommentsSection />  {/* Fetch wewnątrz */}
      </Suspense>
    </div>
  );
}

async function UserInfo() {
  const user = await getUser(); // Streamuje gdy gotowe
  return <UserCard user={user} />;
}

Promise.allSettled() dla fault tolerance:

export default async function Dashboard() {
  const results = await Promise.allSettled([
    getUser(),
    getPosts(),
    getAnalytics(), // Może failować
  ]);

  const user = results[0].status === 'fulfilled' ? results[0].value : null;
  const posts = results[1].status === 'fulfilled' ? results[1].value : [];
  const analytics = results[2].status === 'fulfilled' ? results[2].value : null;

  return (
    <div>
      {user && <UserCard user={user} />}
      <PostsList posts={posts} />
      {analytics && <Analytics data={analytics} />}
    </div>
  );
}

22. Czym są Server Actions i jak ich używać do mutacji danych?

Odpowiedź w 30 sekund:

Server Actions to async funkcje z dyrektywą 'use server' wywoływane z klienta, ale wykonywane na serwerze. Służą do mutacji: formularze, CRUD, walidacja. Automatycznie obsługują rewalidację cache i działają bez JavaScript (progressive enhancement).

Odpowiedź w 2 minuty:

Definiowanie Server Action:

// app/actions.ts
'use server';

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  // Walidacja
  if (!title || title.length < 3) {
    return { error: 'Tytuł musi mieć min. 3 znaki' };
  }

  // Zapis do DB
  const post = await db.post.create({
    data: { title, content },
  });

  // Rewalidacja cache
  revalidatePath('/posts');

  return { success: true, post };
}

Użycie w formularzu (bez JS!):

// app/posts/new/page.tsx
import { createPost } from '../actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Tytuł" required />
      <textarea name="content" placeholder="Treść" />
      <button type="submit">Opublikuj</button>
    </form>
  );
  // Działa nawet z wyłączonym JavaScript!
}

Z obsługą stanu (Client Component):

'use client';

import { useFormState, useFormStatus } from 'react-dom';
import { createPost } from '../actions';

export function PostForm() {
  const [state, formAction] = useFormState(createPost, null);

  return (
    <form action={formAction}>
      <input name="title" />
      {state?.error && <p className="text-red-500">{state.error}</p>}
      <SubmitButton />
    </form>
  );
}

function SubmitButton() {
  const { pending } = useFormStatus();
  return (
    <button disabled={pending}>
      {pending ? 'Zapisywanie...' : 'Zapisz'}
    </button>
  );
}

Server Action wywoływana programowo:

'use client';

import { deletePost } from '../actions';

export function DeleteButton({ postId }) {
  const handleDelete = async () => {
    if (confirm('Na pewno?')) {
      await deletePost(postId);
    }
  };

  return <button onClick={handleDelete}>Usuń</button>;
}

Inline Server Action:

// app/posts/[id]/page.tsx
export default function PostPage({ params }) {
  async function likePost() {
    'use server';
    await db.post.update({
      where: { id: params.id },
      data: { likes: { increment: 1 } },
    });
    revalidatePath(`/posts/${params.id}`);
  }

  return (
    <form action={likePost}>
      <button>❤️ Like</button>
    </form>
  );
}

Optymalizacja i Wydajność

23. Jak działa komponent next/image i jakie oferuje optymalizacje?

Odpowiedź w 30 sekund:

next/image automatycznie optymalizuje obrazy: lazy loading, responsive srcset, konwersja do WebP/AVIF, cache na edge. Wymaga określenia width/height (lub fill) dla uniknięcia layout shift. Obrazy są optymalizowane on-demand, nie w build time.

Odpowiedź w 2 minuty:

Podstawowe użycie:

import Image from 'next/image';

// Lokalne obrazy (automatyczne wymiary)
import heroImage from '@/public/hero.jpg';

export function Hero() {
  return (
    <Image
      src={heroImage}
      alt="Hero image"
      placeholder="blur" // Automatyczny blur placeholder
    />
  );
}

// Remote images (wymagane wymiary)
export function Avatar({ src }) {
  return (
    <Image
      src={src}
      alt="Avatar"
      width={100}
      height={100}
      className="rounded-full"
    />
  );
}

Fill mode dla responsive:

export function ProductImage({ src }) {
  return (
    <div className="relative aspect-square">
      <Image
        src={src}
        alt="Product"
        fill
        className="object-cover"
        sizes="(max-width: 768px) 100vw, 50vw"
      />
    </div>
  );
}

Optymalizacje out-of-the-box:

Funkcja Opis
Lazy loading Domyślne, loading="eager" wyłącza
Responsive Automatyczny srcset
Format WebP/AVIF z fallback
Size optimization Na żądanie, cache na edge
Blur placeholder Dla lokalnych placeholder="blur"
Priority priority dla LCP images

Konfiguracja dla external images:

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
        pathname: '/products/**',
      },
    ],
    formats: ['image/avif', 'image/webp'],
  },
};

Priority dla LCP:

// Największy obraz above the fold
<Image
  src="/hero.jpg"
  alt="Hero"
  priority // Preload, bez lazy loading
  fill
/>

24. Czym jest next/font i jakie daje korzyści wydajnościowe?

Odpowiedź w 30 sekund:

next/font automatycznie hostuje fonty lokalnie (zero external requests), stosuje font-display: swap, eliminuje layout shift przez CSS size-adjust. Wspiera Google Fonts i lokalne fonty. Fonty są załadowane w build time - zero runtime cost.

Odpowiedź w 2 minuty:

Google Fonts:

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google';

const inter = Inter({
  subsets: ['latin', 'latin-ext'],
  display: 'swap',
  variable: '--font-inter',
});

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  variable: '--font-roboto-mono',
});

export default function RootLayout({ children }) {
  return (
    <html lang="pl" className={`${inter.variable} ${robotoMono.variable}`}>
      <body className={inter.className}>{children}</body>
    </html>
  );
}

CSS variables:

/* globals.css */
:root {
  --font-sans: var(--font-inter);
  --font-mono: var(--font-roboto-mono);
}

body {
  font-family: var(--font-sans);
}

code {
  font-family: var(--font-mono);
}

Lokalne fonty:

import localFont from 'next/font/local';

const customFont = localFont({
  src: [
    {
      path: './fonts/CustomFont-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: './fonts/CustomFont-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
  variable: '--font-custom',
});

Korzyści:

Bez next/font Z next/font
Request do Google Fonts Self-hosted (zero external)
FOUT/FOIT Automatyczny swap + size-adjust
Layout shift Zero CLS
Runtime loading Build time optimization
Blocking render Non-blocking

Tailwind integration:

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)'],
        mono: ['var(--font-roboto-mono)'],
      },
    },
  },
};

25. Jak zoptymalizować ładowanie skryptów zewnętrznych w Next.js?

Odpowiedź w 30 sekund:

Używaj next/script ze strategiami: beforeInteractive (krytyczne, przed hydracją), afterInteractive (domyślne, po hydracii), lazyOnload (niski priorytet, po load). Dla analytics używaj afterInteractive, dla chat widgets lazyOnload.

Odpowiedź w 2 minuty:

Strategie ładowania:

import Script from 'next/script';

// Przed hydracją - blokuje, użyj oszczędnie
<Script
  src="/critical-polyfill.js"
  strategy="beforeInteractive"
/>

// Po hydracii - domyślne dla większości
<Script
  src="https://www.google-analytics.com/analytics.js"
  strategy="afterInteractive"
/>

// Po window.onload - niski priorytet
<Script
  src="https://widget.intercom.io/widget/xxx"
  strategy="lazyOnload"
/>

// Web Worker - off main thread
<Script
  src="/heavy-computation.js"
  strategy="worker"
/>

Google Analytics:

// app/layout.tsx
import Script from 'next/script';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}

        <Script
          src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_ID}`}
          strategy="afterInteractive"
        />
        <Script id="google-analytics" strategy="afterInteractive">
          {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${process.env.NEXT_PUBLIC_GA_ID}');
          `}
        </Script>
      </body>
    </html>
  );
}

Event handlers:

<Script
  src="https://example.com/widget.js"
  strategy="lazyOnload"
  onLoad={() => {
    console.log('Script loaded');
    initWidget();
  }}
  onError={() => {
    console.error('Script failed');
  }}
/>

Inline scripts:

<Script id="show-banner" strategy="afterInteractive">
  {`document.getElementById('banner').style.display = 'block'`}
</Script>

Best practices:

  • Analytics: afterInteractive
  • Chat widgets: lazyOnload
  • Polyfills (jeśli potrzebne): beforeInteractive
  • Heavy SDKs: lazyOnload lub worker

26. Jakie techniki optymalizacji bundle oferuje Next.js?

Odpowiedź w 30 sekund:

Next.js automatycznie: code splitting per route, tree shaking, minifikacja, dead code elimination. Dynamiczne importy z next/dynamic dla lazy loading komponentów. Bundle analyzer (@next/bundle-analyzer) pokazuje co zajmuje miejsce.

Odpowiedź w 2 minuty:

Automatyczne optymalizacje:

✅ Code splitting per route
✅ Tree shaking nieużywanego kodu
✅ Minifikacja (Terser/SWC)
✅ CSS minification
✅ Automatic polyfills (modern browsers)

Dynamic imports:

import dynamic from 'next/dynamic';

// Lazy load komponentu
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <p>Ładowanie wykresu...</p>,
  ssr: false, // Tylko client-side (np. dla bibliotek bez SSR)
});

export default function Dashboard() {
  return (
    <div>
      <h1>Dashboard</h1>
      <HeavyChart /> {/* Ładowany gdy widoczny */}
    </div>
  );
}

Named exports:

const Modal = dynamic(() =>
  import('@/components/modals').then(mod => mod.ConfirmModal)
);

Bundle analyzer:

// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
  enabled: process.env.ANALYZE === 'true',
});

module.exports = withBundleAnalyzer({
  // config
});

// Uruchomienie:
// ANALYZE=true npm run build

Import optimization:

// ❌ Importuje całą bibliotekę
import { format } from 'date-fns';

// ✅ Importuje tylko potrzebną funkcję
import format from 'date-fns/format';

// ✅ Lub z modularizeImports w next.config.js
module.exports = {
  modularizeImports: {
    'date-fns': {
      transform: 'date-fns/{{member}}',
    },
    'lodash': {
      transform: 'lodash/{{member}}',
    },
  },
};

Server Components = zero client JS:

// Ten komponent NIE trafia do bundle klienta
// app/products/page.tsx (Server Component)
import { marked } from 'marked'; // Duża lib, ale tylko na serwerze

export default async function Page() {
  const content = marked(markdown);
  return <div dangerouslySetInnerHTML={{ __html: content }} />;
}

27. Jak skonfigurować lazy loading komponentów w Next.js?

Odpowiedź w 30 sekund:

Używaj next/dynamic dla komponentów i React lazy() + Suspense. Dynamic import dzieli bundle - komponent ładuje się gdy potrzebny. Opcja ssr: false dla komponentów client-only (np. wykresy bez SSR support).

Odpowiedź w 2 minuty:

next/dynamic (zalecane):

import dynamic from 'next/dynamic';

// Podstawowy lazy loading
const DynamicComponent = dynamic(() => import('@/components/Heavy'));

// Z loading state
const Chart = dynamic(() => import('@/components/Chart'), {
  loading: () => <div className="animate-pulse h-64 bg-gray-200" />,
});

// Bez SSR (dla bibliotek browser-only)
const MapComponent = dynamic(() => import('@/components/Map'), {
  ssr: false,
  loading: () => <p>Ładowanie mapy...</p>,
});

export default function Page() {
  return (
    <div>
      <h1>Dashboard</h1>
      <Chart />
      <MapComponent />
    </div>
  );
}

Warunkowy lazy loading:

'use client';

import dynamic from 'next/dynamic';
import { useState } from 'react';

const Modal = dynamic(() => import('@/components/Modal'));

export function Page() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <button onClick={() => setShowModal(true)}>
        Otwórz modal
      </button>

      {/* Modal ładuje się dopiero gdy showModal = true */}
      {showModal && <Modal onClose={() => setShowModal(false)} />}
    </div>
  );
}

React lazy() z Suspense (alternatywa):

'use client';

import { lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('@/components/Heavy'));

export function Page() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
}

Preloading na hover:

'use client';

import dynamic from 'next/dynamic';
import { useState } from 'react';

const HeavyModal = dynamic(() => import('@/components/HeavyModal'));

// Preload funkcja
const preloadModal = () => {
  import('@/components/HeavyModal');
};

export function Button() {
  const [show, setShow] = useState(false);

  return (
    <>
      <button
        onMouseEnter={preloadModal} // Preload na hover
        onClick={() => setShow(true)}
      >
        Otwórz
      </button>
      {show && <HeavyModal />}
    </>
  );
}

Metadata i SEO

28. Jak definiować metadata statyczne i dynamiczne w App Router?

Odpowiedź w 30 sekund:

Eksportuj metadata object dla statycznych wartości lub generateMetadata() async function dla dynamicznych. Metadata jest dziedziczone i mergowane z parent layoutów. App Router automatycznie generuje <head> z title, description, Open Graph, Twitter cards.

Odpowiedź w 2 minuty:

Statyczne metadata:

// app/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    template: '%s | Moja Aplikacja',
    default: 'Moja Aplikacja',
  },
  description: 'Opis aplikacji',
  keywords: ['next.js', 'react', 'typescript'],
  authors: [{ name: 'Jan Kowalski' }],
  openGraph: {
    type: 'website',
    locale: 'pl_PL',
    siteName: 'Moja Aplikacja',
  },
  twitter: {
    card: 'summary_large_image',
    creator: '@jankowalski',
  },
};

Dynamiczne metadata:

// app/products/[id]/page.tsx
import type { Metadata } from 'next';

type Props = {
  params: { id: string };
};

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const product = await getProduct(params.id);

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      images: [product.image],
    },
  };
}

export default async function ProductPage({ params }: Props) {
  const product = await getProduct(params.id);
  return <ProductDetails product={product} />;
}

Dziedziczenie i merge:

// app/layout.tsx
export const metadata = {
  title: {
    template: '%s | Shop',
    default: 'Shop',
  },
};

// app/products/page.tsx
export const metadata = {
  title: 'Produkty', // Wynik: "Produkty | Shop"
};

// app/products/[id]/page.tsx
export async function generateMetadata({ params }) {
  const product = await getProduct(params.id);
  return {
    title: product.name, // Wynik: "iPhone 15 | Shop"
  };
}

Pełny przykład:

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: 'Tytuł strony',
  description: 'Opis strony',
  robots: {
    index: true,
    follow: true,
  },
  openGraph: {
    title: 'OG Title',
    description: 'OG Description',
    images: ['/og-image.jpg'],
  },
  twitter: {
    card: 'summary_large_image',
    title: 'Twitter Title',
    images: ['/twitter-image.jpg'],
  },
  alternates: {
    canonical: '/products',
    languages: {
      'en': '/en/products',
      'pl': '/pl/products',
    },
  },
};

29. Czym jest plik opengraph-image i jak go używać?

Odpowiedź w 30 sekund:

opengraph-image.tsx to specjalny plik w App Router generujący OG images dynamicznie za pomocą JSX. Next.js używa @vercel/og (Satori) do renderowania React do obrazu. Możesz też użyć statycznego pliku opengraph-image.png.

Odpowiedź w 2 minuty:

Statyczny OG image:

app/
├── opengraph-image.png     # /opengraph-image
├── twitter-image.png       # /twitter-image
├── products/
│   └── [id]/
│       └── opengraph-image.png  # Per-product

Dynamiczny OG image (JSX):

// app/opengraph-image.tsx
import { ImageResponse } from 'next/og';

export const runtime = 'edge';

export const size = {
  width: 1200,
  height: 630,
};

export const contentType = 'image/png';

export default function OGImage() {
  return new ImageResponse(
    (
      <div
        style={{
          height: '100%',
          width: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          backgroundColor: '#1a1a2e',
          color: 'white',
        }}
      >
        <h1 style={{ fontSize: 64 }}>Moja Aplikacja</h1>
        <p style={{ fontSize: 32 }}>Najlepszy produkt na rynku</p>
      </div>
    ),
    { ...size }
  );
}

Dynamiczny per-product:

// app/products/[id]/opengraph-image.tsx
import { ImageResponse } from 'next/og';

export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

export default async function OGImage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          height: '100%',
          width: '100%',
          backgroundColor: 'white',
          padding: 48,
        }}
      >
        <img
          src={product.image}
          width={400}
          height={400}
          style={{ objectFit: 'cover' }}
        />
        <div style={{ marginLeft: 48 }}>
          <h1 style={{ fontSize: 48 }}>{product.name}</h1>
          <p style={{ fontSize: 32, color: 'green' }}>{product.price} zł</p>
        </div>
      </div>
    ),
    { ...size }
  );
}

Z custom fontem:

export default async function OGImage() {
  const font = await fetch(
    new URL('./Inter-Bold.ttf', import.meta.url)
  ).then((res) => res.arrayBuffer());

  return new ImageResponse(
    (<div style={{ fontFamily: 'Inter' }}>Hello</div>),
    {
      ...size,
      fonts: [{ name: 'Inter', data: font, weight: 700 }],
    }
  );
}

30. Jak wygenerować sitemap.xml i robots.txt w Next.js?

Odpowiedź w 30 sekund:

Utwórz sitemap.ts eksportujący async function zwracającą tablicę URL-i. Dla robots utwórz robots.ts eksportujący obiekt z rules i sitemap URL. Oba pliki w /app generują dynamicznie /sitemap.xml i /robots.txt.

Odpowiedź w 2 minuty:

Sitemap statyczny:

// app/sitemap.ts
import { MetadataRoute } from 'next';

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://example.com',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1,
    },
    {
      url: 'https://example.com/about',
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
  ];
}

Sitemap dynamiczny (z DB):

// app/sitemap.ts
import { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const products = await getProducts();
  const posts = await getPosts();

  const productUrls = products.map((product) => ({
    url: `https://example.com/products/${product.slug}`,
    lastModified: product.updatedAt,
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  }));

  const postUrls = posts.map((post) => ({
    url: `https://example.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'monthly' as const,
    priority: 0.6,
  }));

  return [
    {
      url: 'https://example.com',
      lastModified: new Date(),
      priority: 1,
    },
    ...productUrls,
    ...postUrls,
  ];
}

Multiple sitemaps (dla dużych stron):

// app/sitemap.ts
export async function generateSitemaps() {
  const productCount = await getProductCount();
  const numSitemaps = Math.ceil(productCount / 50000);

  return Array.from({ length: numSitemaps }, (_, i) => ({ id: i }));
}

export default async function sitemap({ id }: { id: number }) {
  const products = await getProductsByPage(id, 50000);
  // ...
}
// Generuje: /sitemap/0.xml, /sitemap/1.xml, etc.

Robots.txt:

// app/robots.ts
import { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',
        allow: '/',
        disallow: ['/admin/', '/api/', '/private/'],
      },
      {
        userAgent: 'Googlebot',
        allow: '/',
      },
    ],
    sitemap: 'https://example.com/sitemap.xml',
  };
}

31. Jak zaimplementować canonical URLs i hreflang?

Odpowiedź w 30 sekund:

Canonical URL w metadata.alternates.canonical. Hreflang dla multi-language w metadata.alternates.languages. Next.js automatycznie generuje odpowiednie <link> tagi. Dynamicznie ustawiasz w generateMetadata().

Odpowiedź w 2 minuty:

Canonical URL:

// app/products/[slug]/page.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  alternates: {
    canonical: '/products/iphone-15', // Relatywne do metadataBase
  },
};

// Lub dynamicznie:
export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    alternates: {
      canonical: `/products/${params.slug}`,
    },
  };
}

Hreflang dla multi-language:

// app/[locale]/products/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const { locale, slug } = params;

  return {
    alternates: {
      canonical: `/${locale}/products/${slug}`,
      languages: {
        'en': `/en/products/${slug}`,
        'pl': `/pl/products/${slug}`,
        'de': `/de/products/${slug}`,
        'x-default': `/en/products/${slug}`,
      },
    },
  };
}

Wygenerowany HTML:

<link rel="canonical" href="https://example.com/pl/products/iphone-15" />
<link rel="alternate" hreflang="en" href="https://example.com/en/products/iphone-15" />
<link rel="alternate" hreflang="pl" href="https://example.com/pl/products/iphone-15" />
<link rel="alternate" hreflang="de" href="https://example.com/de/products/iphone-15" />
<link rel="alternate" hreflang="x-default" href="https://example.com/en/products/iphone-15" />

MetadataBase (wymagane dla relative URLs):

// app/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  // Teraz canonical: '/products' → https://example.com/products
};

Z media alternates (RSS, JSON):

export const metadata: Metadata = {
  alternates: {
    canonical: '/blog',
    types: {
      'application/rss+xml': '/blog/feed.xml',
      'application/json': '/blog/feed.json',
    },
  },
};

API i Backend

32. Jak tworzyć Route Handlers (API routes) w App Router?

Odpowiedź w 30 sekund:

Utwórz route.ts w folderze /app. Eksportuj funkcje nazwane po metodach HTTP: GET, POST, PUT, DELETE, PATCH. Funkcje otrzymują Request i zwracają Response. Route Handlers zastępują Pages Router /pages/api.

Odpowiedź w 2 minuty:

Podstawowy Route Handler:

// app/api/products/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const products = await getProducts();
  return NextResponse.json(products);
}

export async function POST(request: Request) {
  const body = await request.json();
  const product = await createProduct(body);
  return NextResponse.json(product, { status: 201 });
}

Dynamic route:

// app/api/products/[id]/route.ts
import { NextResponse } from 'next/server';

export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const product = await getProduct(params.id);

  if (!product) {
    return NextResponse.json(
      { error: 'Not found' },
      { status: 404 }
    );
  }

  return NextResponse.json(product);
}

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } }
) {
  await deleteProduct(params.id);
  return new Response(null, { status: 204 });
}

Query params i headers:

// app/api/search/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const query = searchParams.get('q');
  const page = searchParams.get('page') || '1';

  // Headers
  const authHeader = request.headers.get('authorization');

  const results = await search(query, parseInt(page));

  return NextResponse.json(results, {
    headers: {
      'Cache-Control': 'max-age=60',
    },
  });
}

Cookies:

// app/api/auth/route.ts
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';

export async function POST(request: Request) {
  const { token } = await request.json();

  cookies().set('session', token, {
    httpOnly: true,
    secure: true,
    sameSite: 'lax',
    maxAge: 60 * 60 * 24 * 7, // 7 dni
  });

  return NextResponse.json({ success: true });
}

export async function DELETE() {
  cookies().delete('session');
  return NextResponse.json({ success: true });
}

33. Czym różnią się Route Handlers od Server Actions?

Odpowiedź w 30 sekund:

Route Handlers to REST endpoints (GET/POST/PUT) wywoływane przez fetch, dostępne publicznie. Server Actions to RPC-style funkcje wywoływane bezpośrednio z komponentów, automatycznie obsługują formularz i rewalidację. Używaj Actions do mutacji, Handlers do REST API.

Odpowiedź w 2 minuty:

Porównanie:

Cecha Route Handler Server Action
Wywołanie fetch('/api/...') Bezpośrednie z komponentu
HTTP methods GET, POST, PUT, DELETE Tylko POST (wewnętrznie)
URL Publiczny endpoint Brak publicznego URL
Użycie REST API, webhooks Formularze, mutacje UI
Rewalidacja Ręczna Automatyczna integracja
Progressive Enhancement Nie Tak (działa bez JS)

Route Handler - REST API:

// app/api/products/route.ts
export async function GET() {
  const products = await db.product.findMany();
  return Response.json(products);
}

export async function POST(request: Request) {
  const data = await request.json();
  const product = await db.product.create({ data });
  return Response.json(product, { status: 201 });
}

// Wywołanie:
// fetch('/api/products', { method: 'POST', body: JSON.stringify(data) })

Server Action - mutacja z UI:

// app/actions.ts
'use server';

export async function createProduct(formData: FormData) {
  const name = formData.get('name');
  const product = await db.product.create({ data: { name } });
  revalidatePath('/products');
  return product;
}

// app/products/new/page.tsx
import { createProduct } from '../actions';

export default function NewProduct() {
  return (
    <form action={createProduct}>
      <input name="name" />
      <button type="submit">Utwórz</button>
    </form>
  );
}
// Działa bez JavaScript!

Kiedy używać czego:

Scenariusz Wybór
Public REST API Route Handler
Webhook od Stripe Route Handler
Formularz kontaktowy Server Action
Like/Unlike button Server Action
Mobile app backend Route Handler
Admin dashboard CRUD Server Action

34. Jak obsługiwać różne metody HTTP w Route Handlers?

Odpowiedź w 30 sekund:

Eksportuj osobne funkcje dla każdej metody: GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. Każda funkcja otrzymuje Request i opcjonalnie context z params. Nieobsługiwane metody automatycznie zwracają 405 Method Not Allowed.

Odpowiedź w 2 minuty:

Wszystkie metody w jednym pliku:

// app/api/products/[id]/route.ts
import { NextResponse } from 'next/server';

type Context = { params: { id: string } };

// GET /api/products/123
export async function GET(request: Request, { params }: Context) {
  const product = await db.product.findUnique({
    where: { id: params.id },
  });

  if (!product) {
    return NextResponse.json({ error: 'Not found' }, { status: 404 });
  }

  return NextResponse.json(product);
}

// PUT /api/products/123 (full update)
export async function PUT(request: Request, { params }: Context) {
  const body = await request.json();

  const product = await db.product.update({
    where: { id: params.id },
    data: body,
  });

  return NextResponse.json(product);
}

// PATCH /api/products/123 (partial update)
export async function PATCH(request: Request, { params }: Context) {
  const body = await request.json();

  const product = await db.product.update({
    where: { id: params.id },
    data: body, // Tylko przekazane pola
  });

  return NextResponse.json(product);
}

// DELETE /api/products/123
export async function DELETE(request: Request, { params }: Context) {
  await db.product.delete({ where: { id: params.id } });
  return new Response(null, { status: 204 });
}

// OPTIONS (CORS preflight)
export async function OPTIONS() {
  return new Response(null, {
    headers: {
      'Access-Control-Allow-Origin': '*',
      'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  });
}

HEAD dla metadata:

// HEAD /api/products/123
export async function HEAD(request: Request, { params }: Context) {
  const product = await db.product.findUnique({
    where: { id: params.id },
    select: { updatedAt: true },
  });

  return new Response(null, {
    headers: {
      'Last-Modified': product?.updatedAt.toUTCString() || '',
    },
  });
}

35. Jak zaimplementować API z walidacją i obsługą błędów?

Odpowiedź w 30 sekund:

Używaj Zod do walidacji body/params. Try-catch dla obsługi błędów. Zwracaj odpowiednie status codes (400 dla validation, 404 dla not found, 500 dla server errors). Stwórz helper do formatowania błędów.

Odpowiedź w 2 minuty:

Walidacja z Zod:

// lib/validations.ts
import { z } from 'zod';

export const createProductSchema = z.object({
  name: z.string().min(2, 'Nazwa min. 2 znaki').max(100),
  price: z.number().positive('Cena musi być dodatnia'),
  description: z.string().optional(),
  categoryId: z.string().uuid('Nieprawidłowe ID kategorii'),
});

export type CreateProductInput = z.infer<typeof createProductSchema>;

Route Handler z walidacją:

// app/api/products/route.ts
import { NextResponse } from 'next/server';
import { createProductSchema } from '@/lib/validations';
import { ZodError } from 'zod';

export async function POST(request: Request) {
  try {
    const body = await request.json();

    // Walidacja
    const validatedData = createProductSchema.parse(body);

    // Tworzenie produktu
    const product = await db.product.create({
      data: validatedData,
    });

    return NextResponse.json(product, { status: 201 });

  } catch (error) {
    // Błąd walidacji Zod
    if (error instanceof ZodError) {
      return NextResponse.json(
        {
          error: 'Validation error',
          details: error.errors.map(e => ({
            field: e.path.join('.'),
            message: e.message,
          })),
        },
        { status: 400 }
      );
    }

    // Błąd bazy danych
    if (error instanceof Prisma.PrismaClientKnownRequestError) {
      if (error.code === 'P2002') {
        return NextResponse.json(
          { error: 'Product already exists' },
          { status: 409 }
        );
      }
    }

    // Nieoczekiwany błąd
    console.error('Unexpected error:', error);
    return NextResponse.json(
      { error: 'Internal server error' },
      { status: 500 }
    );
  }
}

Helper do błędów:

// lib/api-response.ts
export function errorResponse(message: string, status: number) {
  return NextResponse.json({ error: message }, { status });
}

export function validationError(errors: Array<{ field: string; message: string }>) {
  return NextResponse.json({ error: 'Validation failed', details: errors }, { status: 400 });
}

export function notFound(resource: string) {
  return NextResponse.json({ error: `${resource} not found` }, { status: 404 });
}

// Użycie:
if (!product) {
  return notFound('Product');
}

Konfiguracja i Deployment

36. Jakie są kluczowe opcje w pliku next.config.js?

Odpowiedź w 30 sekund:

Kluczowe: images.remotePatterns (external images), redirects/rewrites (routing), env (environment variables), experimental (nowe features). Także output: 'standalone' dla Docker, transpilePackages dla ESM, modularizeImports dla tree-shaking.

Odpowiedź w 2 minuty:

Podstawowa konfiguracja:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  // Obrazy z external domains
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
      },
      {
        protocol: 'https',
        hostname: '**.cloudinary.com',
      },
    ],
  },

  // Redirecty (301)
  async redirects() {
    return [
      {
        source: '/old-blog/:slug',
        destination: '/blog/:slug',
        permanent: true,
      },
    ];
  },

  // Rewrites (proxy, bez zmiany URL)
  async rewrites() {
    return [
      {
        source: '/api/:path*',
        destination: 'https://backend.example.com/:path*',
      },
    ];
  },

  // Environment variables (dostępne w runtime)
  env: {
    CUSTOM_VAR: 'value',
  },

  // Headers
  async headers() {
    return [
      {
        source: '/:path*',
        headers: [
          { key: 'X-Frame-Options', value: 'DENY' },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

Zaawansowane opcje:

const nextConfig = {
  // Standalone output dla Docker
  output: 'standalone',

  // Transpilacja ESM packages
  transpilePackages: ['some-esm-package'],

  // Tree-shaking dla dużych bibliotek
  modularizeImports: {
    'lodash': {
      transform: 'lodash/{{member}}',
    },
    '@mui/icons-material': {
      transform: '@mui/icons-material/{{member}}',
    },
  },

  // Experimental features
  experimental: {
    ppr: true, // Partial Prerendering
    serverActions: {
      bodySizeLimit: '2mb',
    },
  },

  // Webpack customization
  webpack: (config, { isServer }) => {
    if (!isServer) {
      config.resolve.fallback = { fs: false };
    }
    return config;
  },
};

37. Jak skonfigurować zmienne środowiskowe w Next.js?

Odpowiedź w 30 sekund:

.env.local dla local, .env.production dla prod. Zmienne z NEXT_PUBLIC_ są dostępne w przeglądarce. Bez prefixu - tylko server-side. Dostęp przez process.env.VAR_NAME. Waliduj zmienne na starcie aplikacji.

Odpowiedź w 2 minuty:

Pliki env (priorytet):

.env                # Domyślne dla wszystkich
.env.local          # Lokalne (gitignore!)
.env.development    # Dev tylko
.env.production     # Prod tylko
.env.test           # Testy

Server-side vs Client-side:

# .env.local

# Server-side only (bezpieczne)
DATABASE_URL="postgresql://..."
API_SECRET_KEY="secret123"
STRIPE_SECRET_KEY="sk_..."

# Client-side (NEXT_PUBLIC_ prefix)
NEXT_PUBLIC_API_URL="https://api.example.com"
NEXT_PUBLIC_STRIPE_PUBLIC_KEY="pk_..."

Użycie:

// Server Component / API Route
const dbUrl = process.env.DATABASE_URL;
const secret = process.env.API_SECRET_KEY;

// Client Component
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
// process.env.DATABASE_URL → undefined w kliencie!

Walidacja z Zod:

// lib/env.ts
import { z } from 'zod';

const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  API_SECRET_KEY: z.string().min(32),
  NEXT_PUBLIC_API_URL: z.string().url(),
});

export const env = envSchema.parse({
  DATABASE_URL: process.env.DATABASE_URL,
  API_SECRET_KEY: process.env.API_SECRET_KEY,
  NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL,
});

// Użycie:
import { env } from '@/lib/env';
const url = env.DATABASE_URL; // Typed!

Runtime env (dla Docker):

// next.config.js
module.exports = {
  // Dostępne w runtime, nie w build time
  serverRuntimeConfig: {
    apiSecret: process.env.API_SECRET,
  },
  publicRuntimeConfig: {
    apiUrl: process.env.NEXT_PUBLIC_API_URL,
  },
};

// Użycie:
import getConfig from 'next/config';
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();

38. Czym różni się deployment na Vercel od self-hosted?

Odpowiedź w 30 sekund:

Vercel: zero-config, automatyczny edge/serverless, preview deployments, analytics. Self-hosted: next build && next start, wymaga Node.js server, brak edge functions (lub dodatkowa konfiguracja). Docker z output: 'standalone' dla self-hosted.

Odpowiedź w 2 minuty:

Vercel (managed):

✅ Zero configuration
✅ Automatic edge functions
✅ Preview deployments (per PR)
✅ Automatic HTTPS, CDN
✅ ISR działa out-of-the-box
✅ Analytics, Speed Insights
✅ Image Optimization included
❌ Vendor lock-in
❌ Koszty przy dużym ruchu

Self-hosted (Node.js):

# Build
npm run build

# Start (wymaga Node.js server)
npm start
# lub
node .next/standalone/server.js
✅ Pełna kontrola
✅ Własna infrastruktura
✅ Brak vendor lock-in
❌ Musisz zarządzać: skalowanie, SSL, CDN
❌ Image Optimization wymaga konfiguracji
❌ ISR wymaga persystentnego cache

Docker deployment:

# Dockerfile
FROM node:18-alpine AS base

FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV production

COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static

EXPOSE 3000
CMD ["node", "server.js"]
// next.config.js dla Docker
module.exports = {
  output: 'standalone', // Minimalny output
};

Porównanie features:

Feature Vercel Self-hosted
Edge Functions ✅ Auto ❌ / Custom
ISR ✅ Auto Wymaga setup
Image Optimization Wymaga loader
Preview Deploys CI/CD setup
Middleware ✅ Edge Node.js

39. Jak skonfigurować Next.js do pracy z Docker?

Odpowiedź w 30 sekund:

Ustaw output: 'standalone' w next.config.js. Multi-stage Dockerfile: deps → build → runner. Kopiuj .next/standalone i .next/static. Uruchom node server.js. Dla Docker Compose dodaj health check i volumes dla cache.

Odpowiedź w 2 minuty:

next.config.js:

module.exports = {
  output: 'standalone',
  // Opcjonalnie: wyłącz telemetrię
  // experimental: { outputFileTracingRoot: path.join(__dirname, '../') }
};

Dockerfile (optimized):

# syntax=docker/dockerfile:1

FROM node:20-alpine AS base
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Dependencies
FROM base AS deps
COPY package.json package-lock.json* ./
RUN npm ci --only=production

# Build
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY . .

ENV NEXT_TELEMETRY_DISABLED 1
RUN npm run build

# Production
FROM base AS runner
WORKDIR /app

ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public

# Standalone output
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000
ENV PORT 3000
ENV HOSTNAME "0.0.0.0"

CMD ["node", "server.js"]

docker-compose.yml:

version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/app
      - NEXT_PUBLIC_API_URL=https://api.example.com
    depends_on:
      - db
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3

  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=app

volumes:
  postgres_data:

.dockerignore:

node_modules
.next
.git
*.md
.env*.local

40. Jak monitorować wydajność aplikacji Next.js w produkcji?

Odpowiedź w 30 sekund:

Vercel Analytics i Speed Insights dla managed. Self-hosted: Web Vitals API z useReportWebVitals, custom analytics (Sentry, DataDog). Kluczowe metryki: LCP, FID/INP, CLS, TTFB. Next.js automatycznie raportuje Server Timing headers.

Odpowiedź w 2 minuty:

Vercel Analytics (managed):

// app/layout.tsx
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        {children}
        <Analytics />
        <SpeedInsights />
      </body>
    </html>
  );
}

Web Vitals API (self-hosted):

// app/web-vitals.tsx
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    // Wyślij do swojego analytics
    console.log(metric);

    // Lub do Google Analytics
    gtag('event', metric.name, {
      value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
      event_label: metric.id,
      non_interaction: true,
    });

    // Lub do custom endpoint
    fetch('/api/analytics', {
      method: 'POST',
      body: JSON.stringify(metric),
    });
  });

  return null;
}

Sentry dla error tracking:

// instrumentation.ts
export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    await import('./sentry.server.config');
  }
  if (process.env.NEXT_RUNTIME === 'edge') {
    await import('./sentry.edge.config');
  }
}

Custom performance logging:

// middleware.ts
import { NextResponse } from 'next/server';

export function middleware(request) {
  const start = Date.now();
  const response = NextResponse.next();

  response.headers.set('Server-Timing', `middleware;dur=${Date.now() - start}`);

  return response;
}

Kluczowe metryki do monitorowania:

Metryka Cel Co oznacza
LCP < 2.5s Largest Contentful Paint
FID/INP < 100ms First Input Delay / Interaction to Next Paint
CLS < 0.1 Cumulative Layout Shift
TTFB < 600ms Time to First Byte

Podsumowanie

Next.js w 2026 to znacznie więcej niż "React z SSR". App Router, Server Components, Server Actions i Partial Prerendering zmieniają sposób budowania aplikacji webowych. Na rozmowie rekrutacyjnej musisz znać:

Fundamenty:

  • Różnica Server Components vs Client Components
  • App Router vs Pages Router
  • Strategie renderowania (SSG, SSR, ISR)

Data fetching:

  • Fetch w Server Components z cache
  • Server Actions do mutacji
  • Streaming z Suspense

Optymalizacja:

  • next/image i next/font
  • Dynamic imports i code splitting
  • PPR (Partial Prerendering)

Infrastruktura:

  • Route Handlers (API)
  • Middleware
  • Deployment (Vercel vs self-hosted)

Zobacz też


Chcesz Więcej Pytań z Next.js?

Mamy kompletny zestaw 40 pytań z Next.js - każde z odpowiedzią w formacie "30 sekund / 2 minuty". Idealne do szybkiego przygotowania przed rozmową.

Zdobądź Dostęp do Wszystkich Pytań →

Chcesz więcej pytań rekrutacyjnych?

To tylko jeden temat z naszego kompletnego przewodnika po rozmowach rekrutacyjnych. Uzyskaj dostęp do 800+ pytań z 13 technologii.

Kup pełny dostęp Zobacz bezpłatny podgląd
Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.