Next.js 16 Route Handlers: Kompletný sprievodca API routes v App Routeri (2026)
Praktický sprievodca Route Handlers v Next.js 16 App Routeri: HTTP metódy, cachovanie s use cache direktívou, Zod validácia, CORS v middleware, streaming, Edge vs Node.js runtime a playbook migrácie z pages/api.
Route Handlers sú súborovo definované HTTP endpointy v Next.js 16 App Routeri, ktoré nahrádzajú starý priečinok pages/api a bežia ako natívne Web Request/Response handlery buď v Node.js alebo Edge runtime. V praxi to znamená, že namiesto exportovania jednej default funkcie exportujete pomenované funkcie podľa HTTP metódy (GET, POST, PATCH, DELETE) a Next.js sa postará o routing, streaming aj typovanie. Za posledný rok som migroval štyri produkčné aplikácie z Pages API routes na Route Handlers a tento článok je destilát toho, čo funguje a kde sú landmíny.
Route Handlers žijú v app/api/**/route.ts a exportujú pomenované funkcie podľa HTTP metódy, nie default export.
Od Next.js 15 nie sú GET requesty predvolene cachované; ak chcete statické správanie, musíte explicitne pridať export const dynamic = 'force-static' alebo direktívu 'use cache'.
Používajte Route Handlers pre webhooky, verejné JSON API, streaming a integrácie tretích strán; pre formuláre a UI mutácie zvoľte Server Actions.
Node.js runtime je defaultný v Next.js 16 (Edge je opt-in cez export const runtime = 'edge'), pretože Edge má menší cold-start, ale limity pamäte 128 MB.
Validáciu telesa robte cez Zod (alebo Valibot) na vstupe; CORS riešte v middleware.ts, nie v každom handleri zvlášť.
Migrácia z pages/api na Route Handlers trvá pre stredne veľkú aplikáciu 2 až 5 človekodní vrátane testov.
Čo sú Route Handlers v Next.js 16?
Route Handlers sú serverovo vykonávané funkcie, ktoré Next.js registruje ako HTTP endpointy na základe cesty k súboru route.ts (alebo route.js) v priečinku app/. Súbor app/api/users/route.ts obsluhuje URL /api/users, súbor app/api/users/[id]/route.ts obsluhuje /api/users/42 a tak ďalej. Na rozdiel od starých API routes z Pages Routera, kde ste exportovali jednu default funkciu s handlerom (req, res), tu exportujete pomenované funkcie, jednu pre každú HTTP metódu, ktorú chcete podporovať.
Podporované metódy sú GET, POST, PUT, PATCH, DELETE, HEAD a OPTIONS. Ak vaša aplikácia dostane request na metódu, ktorá nie je exportovaná, Next.js automaticky vráti 405 Method Not Allowed. Handler dostane objekt NextRequest (rozšírenie Web Request) a musí vrátiť Response alebo NextResponse. Nič viac, nič menej. Je to čisté Web Platform API, žiadne Node-špecifické req.body alebo res.json() ako v Expresse.
Prečo tento posun? Vercel a Next.js team chceli zjednotiť runtime medzi klientom, serverom aj edgeom a Web APIs sú jediný spoločný menovateľ. Bonusom je, že tie isté handlery viete testovať priamo cez fetch() vo Vitest teste bez toho, aby ste museli mockovať Node HTTP layer. Detailnejšie sa dočítate v oficiálnej dokumentácii Next.js Route Handlers.
Základná štruktúra súboru route.ts
Najjednoduchší Route Handler má menej ako desať riadkov a vyzerá takto:
// app/api/health/route.ts
import { NextResponse } from 'next/server';
export async function GET() {
return NextResponse.json({
status: 'ok',
timestamp: new Date().toISOString(),
});
}
Tri veci, ktoré si tu treba všimnúť. Po prvé, importujeme NextResponse z next/server, nie z next. Je to hraničný modul s vlastnými dodatočnými pomocníkmi (cookies, redirect, rewrite). Po druhé, funkcia je async, aj keď v tomto príklade nič neawaitujeme; runtime očakáva Promise a synchrónny return by fungoval, ale konzistentne písať async je čistejšie a menej surprise. Po tretie, NextResponse.json() automaticky nastaví Content-Type: application/json a serialisuje objekt, takže nemusíte volať JSON.stringify ručne.
Ak chcete čítať query parametre z URL, dostanete ich cez request.nextUrl.searchParams. Pre dynamické segmenty (napr. [id]) dostane handler druhý argument s objektom { params }, ktorý je od Next.js 15 Promise a treba ho awaitovať:
Tá zmena na Promise<params> je jedno z najčastejších miest, kde ľudia zaseknú migráciu. V Next.js 14 to bol synchrónny objekt, teraz je to Promise a TypeScript vám kričí, kým to nezmeníte. (Zoznámil som sa s tým dôverne pri jednom nočnom hotfixe, ktorý dopadol dobre až po tretej káve.)
HTTP metódy: GET, POST, PATCH, DELETE s príkladmi
Reálny REST endpoint zvyčajne obsluhuje viacero metód v jednom súbore. Poďme si postaviť CRUD pre resource posts:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
// Zoznam príspevkov
export async function GET(request: NextRequest) {
const cursor = request.nextUrl.searchParams.get('cursor');
const posts = await db.post.findMany({
take: 20,
...(cursor && { cursor: { id: cursor }, skip: 1 }),
orderBy: { createdAt: 'desc' },
});
const nextCursor = posts.length === 20 ? posts[19].id : null;
return NextResponse.json({ posts, nextCursor });
}
// Vytvorenie nového príspevku
export async function POST(request: NextRequest) {
const body = await request.json();
const post = await db.post.create({ data: body });
return NextResponse.json(post, { status: 201 });
}
Pre jednotlivý zdroj (PATCH, DELETE) si založíme app/api/posts/[id]/route.ts:
// app/api/posts/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { db } from '@/lib/db';
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const patch = await request.json();
const post = await db.post.update({ where: { id }, data: patch });
return NextResponse.json(post);
}
export async function DELETE(
_request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
await db.post.delete({ where: { id } });
return new NextResponse(null, { status: 204 });
}
Pár praktických poznámok. Pre 204 No Content vraciam new NextResponse(null, ...), pretože NextResponse.json() vždy pridá telo, a 204 so telom je porušenie HTTP špecifikácie, ktoré niektoré proxy odmietnu. Pre POST vraciam 201 Created, nie predvolený 200. A všetky mutácie by mali validovať telo pred zápisom do DB (k tomu sa dostanem v sekcii o Zod).
Route Handlers vs Server Actions: kedy použiť čo?
Toto je najčastejšia otázka, ktorú dostávam pri code review. Odpoveď je jednoduchá: Server Actions pre formuláre a UI mutácie z tej istej Next.js aplikácie; Route Handlers pre všetko ostatné. Tu je porovnávacia tabuľka, ktorú tlačím tímom pri onboardingu:
Vlastnosť
Route Handlers
Server Actions
Vstupný bod
Verejná URL (/api/*)
Zabalené v RSC formulári alebo hooku
HTTP kontrola
Plná (status, headers, streaming)
Abstrahovaná Next.jsom
CSRF ochrana
Musíte doplniť sami
Vstavaná (origin check)
Volanie z tretích strán
Áno (webhooky, mobile app)
Nie
OpenAPI / typová zmluva
Áno (Zod → openapi-ts)
Iba interné
Progressive enhancement
Ručné
Automatické
Cachovanie odpovedí
Áno (cez use cache)
Nie
Reálny príklad: Stripe webhook je Route Handler, pretože Stripe posiela POST z externej infraštruktúry. „Uložiť koncept blogu" tlačidlo je Server Action, pretože je viazané na formulár a nikto ho nikdy nezavolá z curl. Ak si potrebujete osviežiť Server Actions, mám k tomu samostatný praktický sprievodca Server Actions v Next.js 16.
Existuje aj hybridný scenár: verejné API, ktoré vaše vlastné React komponenty aj externí klienti volajú. Tam je Route Handler jasná voľba a Server Action urobíte iba ako tenký wrapper okolo interného volania fetch('/api/...'), ak to prináša výhody progressive enhancement.
Cachovanie a dynamické správanie v Next.js 16
Toto je oblasť, kde Next.js 16 najviac odbočil od predchádzajúcej verzie a kde vidím najviac production incidentov. Krátka verzia: Route Handlers sú predvolene dynamické od Next.js 15. Ak chcete cachovaný GET, musíte to povedať explicitne.
Máte tri páky. Prvá je stará export const dynamic:
// Vždy sa vykoná pri requeste (default)
export const dynamic = 'force-dynamic';
// Cachuje sa počas buildu a slúži zo statického súboru
export const dynamic = 'force-static';
Druhá, a v Next.js 16 odporúčaná, je nová direktíva 'use cache' vnútri handlera, ktorá sa integruje s Cache Components:
// app/api/leaderboard/route.ts
import { NextResponse } from 'next/server';
import { unstable_cacheLife as cacheLife } from 'next/cache';
export async function GET() {
'use cache';
cacheLife('minutes'); // revaliduj po minútach
const rows = await db.$queryRaw`SELECT * FROM leaderboard LIMIT 100`;
return NextResponse.json({ rows });
}
Podrobne som Cache Components a novú direktívu rozobral v článku use cache: sprievodca kešovaním komponentov. Tretia páka je klasické revalidatePath() / revalidateTag(), ktoré po mutácii vyprázdni cache.
Praktické čísla z jedného e-commerce klienta: cachovanie GET endpointu pre listing produktov s cacheLife('hours') zrazilo p95 latenciu z 340 ms na 18 ms a znížilo počet DB queries o 94%. Ale (a toto je dôležité) cache invalidácia po zmene ceny predtým fungovala v Server Actions volaním revalidateTag, po prepnutí na Route Handler sme museli tú istú logiku zopakovať. Cache nie je zadarmo, keď ju musíte manuálne invalidnúť.
Validácia vstupov so Zod
Nikdy neverte tomu, čo klient pošle. Neseriózne aplikácie majú validáciu v handleri ako opozdilé „if body.email doesn't have @…", seriózne aplikácie používajú Zod schema library alebo Valibot a validujú kompletnú štruktúru pred akoukoľvek business logikou. Tu je pattern, ktorý používam vo všetkých svojich projektoch:
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
import { db } from '@/lib/db';
const CreatePostSchema = z.object({
title: z.string().min(1).max(200),
content: z.string().min(1).max(50_000),
tags: z.array(z.string()).max(10).optional(),
});
export async function POST(request: NextRequest) {
let raw: unknown;
try {
raw = await request.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
}
const parsed = CreatePostSchema.safeParse(raw);
if (!parsed.success) {
return NextResponse.json(
{ error: 'Validation failed', issues: parsed.error.issues },
{ status: 422 }
);
}
const post = await db.post.create({ data: parsed.data });
return NextResponse.json(post, { status: 201 });
}
Dve dôležité veci: safeParse namiesto parse, aby sme si sami rozhodli, čo s chybou. A vraciame 422 Unprocessable Entity, nie 400. Prečo? 400 je pre malformed request (napr. rozbitý JSON), 422 je pre syntakticky správny, ale sémanticky neplatný obsah. Táto distinkcia sa vám vypláca, keď píšete OpenAPI dokumentáciu alebo integrujete s klientmi, ktorí rozlišujú retry logiku podľa status kódu.
Streaming odpovedí cez ReadableStream
Route Handlers dokážu streamovať odpoveď postupne, čo je ideálne pre LLM proxy, SSE (Server-Sent Events) alebo veľké CSV exporty. Základný stream vyzerá takto:
Klientská strana potom číta cez fetch s response.body.getReader() alebo cez EventSource API. Streaming funguje aj v Node.js aj v Edge runtime, ale na Edge má tvrdý limit 25 sekúnd na Vercel Hobby / 60 s na Pro. Pre dlho bežiace generovanie musíte alebo prejsť na Node.js runtime s maxDuration = 300, alebo vyhodiť job do queue a klientovi streamovať progress.
Ako riešiť CORS a autentifikáciu?
CORS je téma, kde vidím najviac copy-paste chýb. Nesprávna odpoveď je pridávať Access-Control-Allow-Origin: * do každého handlera. Správna odpoveď je centralizovať to v middleware.ts:
Pre API keys (mobilné klienty, integrácie) čítajte Authorization: Bearer <token> a validujte proti databáze. Nikdy nespoliehajte iba na porovnanie stringu, keďže časová korelácia môže odhaliť platné tokeny.
Edge Runtime vs Node.js Runtime
Next.js 16 defaultuje na Node.js runtime pre Route Handlers, čo je zmena oproti Next.js 14, kde bol Edge často predvolený pre app/api. Prečo? Edge má tvrdé limity: 128 MB pamäte, 25 s CPU na Vercel Hobby, žiadny prístup k Node.js API ako fs, net alebo child_process, žiadny natívny binárny náklad (napr. @node-rs/argon2). Väčšina real-world backendov tieto limity za mesiac trafí.
Edge má význam, keď potrebujete geograficky blízke odpovede s malým payloadom: feature flags, A/B testing, geolocation redirects, jednoduché read-through cache endpointy. Prepnete ho jednou riadkou:
Praktický benchmark z minulého kvartála pre klienta s DACH publikom: prepnutie feature-flag endpointu z Node.js (funkcia v iad1) na Edge s preferredRegion: ['fra1'] zrazilo p50 latenciu z 187 ms na 41 ms. Ale ten istý klient mal Stripe webhook, ktorý sme na Edge musieť zase vrátiť na Node.js, pretože Stripe SDK používa crypto moduly, ktoré nie sú v Edge dostupné. Honestly, u nás v tíme je pravidlo: Edge len tam, kde vieme čo, prečo a s meraním v ruke.
Migrácia z pages/api: playbook krok za krokom
Podľa mojich estimatov trvá migrácia stredne veľkej aplikácie (30 až 50 endpointov) 2 až 5 človekodní. Postupujte v tomto poradí:
Deň 1 (0.5 dňa): Vytvorte app/api/ priečinok paralelne s pages/api/. Next.js oboje toleruje súčasne, nie je to všetko-alebo-nič.
Deň 1 (0.5 dňa): Presuňte jeden triviálny endpoint (health check, ping) ako pilot. Otestujte v produkčnom preview deploymente.
Deň 2 (1 deň): Migrujte GET-only endpointy (listing, detail). Zmeňte req.query na request.nextUrl.searchParams, res.json() na NextResponse.json().
Deň 3 (1 deň): Migrujte mutácie (POST, PATCH, DELETE). Nahraďte req.body parsing za await request.json() + Zod validáciu.
Deň 4 (0.5–1 deň): Vyriešte CORS a middleware centrálne. Vymažte per-handler CORS logiku.
Deň 5 (0.5–1 deň): Zmažte pages/api/, spustite integračné testy, deploy do produkcie s dark launchom (canary %).
Najčastejšie landmíny, na ktoré si dajte pozor: getServerSideProps-style helpers nefungujú v Route Handleroch (žiaden req.session od Iron Session bez adaptera), body parser správanie je iné (žiadne req.body automaticky), a next-connect middleware chain treba nahradiť centralizovaným middleware.ts alebo malým wrapper funkciou. Kompletný changelog jednotlivých breaking changes nájdete v GitHub release notes Next.js.
Často kladené otázky
Sú Route Handlers v Next.js 16 predvolene cachované?
Nie. Od Next.js 15 sú GET Route Handlers predvolene dynamické. Ak chcete cachovať odpoveď, musíte pridať export const dynamic = 'force-static', alebo použiť direktívu 'use cache' vnútri handlera spolu s cacheLife(). POST/PATCH/DELETE sa necachujú nikdy.
Kedy použiť Server Actions namiesto Route Handlers?
Server Actions používajte pre formuláre a UI mutácie z tej istej Next.js aplikácie. Dostávate vstavanú CSRF ochranu, progressive enhancement a čistejšie typy. Route Handlers zvoľte pre verejné API, webhooky, mobile klientov a všetko, čo musí byť zavolané cez HTTP z externej infraštruktúry.
Ako pridám CORS do Next.js Route Handlerov?
Najčistejšie riešenie je centralizovať CORS logiku v middleware.ts s matcher: '/api/:path*'. Middleware zachytí OPTIONS preflight aj bežné requesty a pridá Access-Control-Allow-Origin hlavičky. Nepridávajte CORS do každého handlera zvlášť, pri 30 endpointoch máte 30 miest, kde môžete zabudnúť na preflight.
Fungujú Route Handlers v Edge Runtime?
Áno, pridaním export const runtime = 'edge' do súboru route.ts. Ale Edge má limity: 128 MB pamäte, 25 s CPU (Hobby) alebo 60 s (Pro), žiaden prístup k fs/net/child_process a žiadne natívne binárky. Pre väčšinu backendov je predvolený Node.js runtime lepšia voľba; Edge nechajte pre feature flags a geo-lokálne micro-endpointy.
Ako otestovať Route Handler v unit teste?
Keďže Route Handlers pracujú s natívnym Web Request/Response, môžete ich importovať priamo a zavolať vo Vitest alebo Jest teste bez akéhokoľvek Next.js runtime setupu: const res = await GET(new NextRequest('http://localhost/api/health')). Odpoveď je štandardný Response, takže await res.json() vráti telo. Žiadne mock-req/mock-res knižnice ako v Pages Router ére.
Praktický sprievodca opravou hydratačných chýb v Next.js 16 s React DevTools, useEffect vzorom, dynamic importmi a suppressHydrationWarning. S ukážkami kódu.
Praktický sprievodca Server Actions v Next.js 16: ako písať formuláre s useActionState, validovať vstupy Zod-om, revalidovať cache a vyhnúť sa typickým bezpečnostným chybám v produkčnom kóde.