Route Handlers în Next.js 16: Ghid Complet pentru API REST cu route.ts
Construiește o API REST completă cu Route Handlers în Next.js 16: GET/POST/PATCH/DELETE, validare Zod, autentificare prin cookies, caching opt-in, Edge Runtime și streaming SSE.
Route Handlers în Next.js 16 sunt funcții exportate dintr-un fișier route.ts care răspund la cereri HTTP (GET, POST, PATCH, PUT, DELETE, OPTIONS) folosind API-urile web standard Request și Response. Ele înlocuiesc complet vechile pages/api din Pages Router, suportă atât Edge cât și Node runtime, se integrează nativ cu Server Components și îți oferă control granular asupra cache-ului prin opțiunile dynamic, revalidate și fetchCache. În ghidul ăsta construim împreună o API REST întreagă, cu validare Zod, autentificare și streaming.
Un Route Handler trăiește într-un fișier app/api/<cale>/route.ts și exportă funcții numite după metoda HTTP, de exemplu export async function GET(request: NextRequest).
În Next.js 16, Route Handlers sunt implicit dinamice (nu mai sunt cache-uite automat ca în 14), exact ca fetch. Trebuie să optezi explicit pentru cache cu export const dynamic = 'force-static' sau revalidate = 60.
Segmentele dinamice se obțin din parametrul params, care este un Promise începând cu Next.js 15. Folosește const { id } = await params.
Pentru validare folosește request.json() plus z.safeParse, iar pentru erori returnează NextResponse.json(..., { status: 400 }).
Edge Runtime (export const runtime = 'edge') reduce cold start sub 50 ms, dar nu suportă driverele Node native (pg, mysql2). Folosește clienți HTTP, cum ar fi Neon sau Drizzle HTTP.
Streaming-ul se face cu ReadableStream și răspuns new Response(stream, { headers: { 'Content-Type': 'text/event-stream' } }).
Ce sunt Route Handlers și când le folosești
Route Handlers sunt înlocuitorul modern pentru pages/api/*.ts din Pages Router. Trăiesc în directorul app/, alături de paginile tale, și folosesc convenția de fișier route.ts (sau route.js). Spre deosebire de Server Actions, care sunt invocate ca RPC dintr-o componentă React, Route Handlers expun un endpoint HTTP public, cu URL stabil, accesibil de oriunde: clienți mobili, webhook-uri, alte servicii, sau chiar curl.
Onest, prima dată când am construit o API publică în App Router m-am chinuit câteva ore cu cache-ul, până am realizat că în 16 totul e dinamic implicit. Așa că reține: folosește Route Handlers când ai nevoie de o API REST publică (sau privată), webhook-uri (Stripe, GitHub, Vercel), endpoint-uri pentru upload-uri, sau orice integrare unde un alt sistem trebuie să facă o cerere HTTP către aplicația ta. Dacă în schimb construiești doar un formular submit dintr-o componentă React în aceeași aplicație, citește ghidul nostru despre Server Actions cu validare Zod. Sunt pur și simplu mai potrivite pentru cazul ăla.
În Next.js 16 există o schimbare importantă față de versiunile anterioare: Route Handlers sunt acum implicit dinamice. În Next.js 13/14, un handler GET fără cookies, headers sau search params era cache-uit static la build. Acel comportament a fost eliminat. Toate handler-ele rulează per-request decât dacă optezi explicit prin export const dynamic = 'force-static'. Asta aliniază comportamentul cu fetch și elimină surprizele de cache pe care le raportau dezvoltatorii în vechile RFC-uri.
Primul endpoint GET și POST
Să construim un endpoint simplu pentru articole. Creează fișierul app/api/articles/route.ts:
// app/api/articles/route.ts
import { NextRequest, NextResponse } from 'next/server'
// În realitate ai face fetch din DB; aici inline pentru claritate
const articles = [
{ id: 1, title: 'Route Handlers in Next.js 16', published: true },
{ id: 2, title: 'Server Actions deep dive', published: false },
]
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const onlyPublished = searchParams.get('published') === 'true'
const result = onlyPublished
? articles.filter((a) => a.published)
: articles
return NextResponse.json({ data: result, count: result.length })
}
export async function POST(request: NextRequest) {
const body = await request.json()
const newArticle = { id: articles.length + 1, ...body }
articles.push(newArticle)
return NextResponse.json(newArticle, { status: 201 })
}
Cu serverul pornit (npm run dev), poți accesa http://localhost:3000/api/articles sau http://localhost:3000/api/articles?published=true. NextRequest extinde clasa standard Request cu shortcut-uri utile precum request.cookies, request.geo și request.ip. Pentru detalii oficiale, vezi documentația Route Handlers și Response API pe MDN.
Segmente dinamice și catch-all
Pentru rute parametrizate, folosește notația cu paranteze pătrate. Fișierul app/api/articles/[id]/route.ts capturează un ID:
// app/api/articles/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server'
type RouteContext = { params: Promise<{ id: string }> }
export async function GET(
request: NextRequest,
context: RouteContext
) {
const { id } = await context.params
const numericId = Number(id)
if (Number.isNaN(numericId)) {
return NextResponse.json(
{ error: 'ID-ul trebuie să fie numeric' },
{ status: 400 }
)
}
// ... fetch din DB
return NextResponse.json({ id: numericId, title: 'Exemplu' })
}
export async function DELETE(
request: NextRequest,
context: RouteContext
) {
const { id } = await context.params
// ... șterge din DB
return new Response(null, { status: 204 })
}
Atenție: începând cu Next.js 15, params este un Promise. Codul vechi care folosea context.params.id direct va da warning sau eroare în Next.js 16. Folosește mereu const { id } = await context.params. Am pățit fix bug-ul ăsta migrând o aplicație de la 14 la 16 și mi-a luat o jumătate de oră să-mi dau seama de ce TypeScript bombănea.
Pentru rute catch-all, folosește [...slug]/route.ts (capturează /api/files/a/b/c ca slug: ['a', 'b', 'c']) sau [[...slug]]/route.ts pentru optional catch-all care match-uiește și rădăcina.
Validare cu Zod și gestionarea erorilor
Niciodată să n-ai încredere în body-ul cererii. Validează tot. Zod este standardul de facto în ecosistemul Next.js 16:
// app/api/articles/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
const ArticleSchema = z.object({
title: z.string().min(3).max(200),
content: z.string().min(10),
published: z.boolean().default(false),
tags: z.array(z.string()).max(10).optional(),
})
export async function POST(request: NextRequest) {
let body: unknown
try {
body = await request.json()
} catch {
return NextResponse.json(
{ error: 'JSON invalid' },
{ status: 400 }
)
}
const parsed = ArticleSchema.safeParse(body)
if (!parsed.success) {
return NextResponse.json(
{
error: 'Validation failed',
issues: parsed.error.flatten().fieldErrors,
},
{ status: 422 }
)
}
// parsed.data este complet tipizat
// ... insert în DB
return NextResponse.json({ ok: true }, { status: 201 })
}
Returnează codurile de status corecte: 400 pentru body malformat, 422 pentru validare eșuată, 404 pentru resursă lipsă, 409 pentru conflict, 500 doar pentru erori neașteptate. Un middleware global de error-handling se poate face cu o funcție wrapper:
În Next.js 16, comportamentul implicit este fără cache. Dacă vrei caching, opt-in explicit:
// app/api/stats/route.ts
export const dynamic = 'force-static' // pre-render la build
export const revalidate = 60 // ISR: regenerează la fiecare 60s
export async function GET() {
const stats = await fetchExpensiveStats()
return Response.json(stats)
}
Opțiunile disponibile pentru dynamic:
'auto' (implicit): dinamic, dar permite optimizări dacă handler-ul nu accesează API-uri dinamice.
'force-dynamic': forțează rendering per-request, dezactivează orice cache.
'force-static': forțează caching static; erori dacă handler-ul accesează cookies, headers sau searchParams.
'error': ca auto, dar aruncă eroare dacă încerci o operație dinamică.
Pentru cache pe granularitate de fetch, folosește next.revalidate pe fiecare fetch() sau funcția revalidateTag() ca să invalidezi un grup de cereri când datele se schimbă. Vezi și articolul nostru despre Cache Components cu use cache, cacheLife și cacheTag pentru cum se interconectează cu Route Handlers.
Edge Runtime vs Node Runtime
Fiecare handler poate rula pe Node.js runtime (default) sau pe Edge runtime:
export const runtime = 'edge' // sau 'nodejs' (default)
Caracteristică
Node Runtime
Edge Runtime
Cold start
~200–500 ms
< 50 ms
Memorie maximă
1–3 GB (Vercel)
~128 MB
API-uri Node native (fs, child_process)
Da
Nu
Driver-e DB TCP (pg, mysql2, mongodb)
Da
Nu (doar HTTP: Neon, PlanetScale, Drizzle HTTP)
Streaming nativ
Da
Da
Maximum execution time
până la 300s (Pro)
30s (request) / 25s (background)
Cel mai bun pentru
DB queries, logică grea, integrări SDK
Auth, geolocation, AI streaming, redirects
Alege Edge când latența contează și dependențele sunt compatibile (Web APIs). Alege Node când ai nevoie de pachete npm cu binding-uri native sau de o conexiune persistentă la DB.
Streaming și Server-Sent Events
Route Handlers suportă răspunsuri streaming prin ReadableStream. Util pentru SSE, AI completions sau export-uri mari:
Pe client, consumă-l cu EventSource sau cu fetch().body.getReader() pentru control granular. Pentru integrări AI, AI SDK al Vercel oferă helper-i precum StreamingTextResponse care simplifică acest pattern.
CORS și handler-ul OPTIONS
Dacă API-ul tău e consumat de un domeniu diferit, browser-ul va trimite un preflight OPTIONS. Exportă explicit handler-ul:
Cele două mecanisme par similare, dar rezolvă probleme diferite. Server Actions sunt funcții server invocate ca RPC din componente React în aceeași aplicație Next.js. Au tipuri end-to-end, integrare cu useActionState, și sunt apelate prin POST către același route ca pagina. Route Handlers, în schimb, sunt endpoint-uri HTTP publice cu URL fix, ideale pentru consumatori externi: aplicații mobile, webhook-uri, scripturi cron.
Regulă de aur (mea, după câteva proiecte): dacă singurul tău consumator este aceeași aplicație Next.js și formularul tău este randat de React, folosește Server Actions. Pentru orice altceva (mobile app, integrare third-party, webhook, public API), folosește Route Handlers.
Întrebări frecvente
Care e diferența dintre Route Handlers și pages/api în Next.js 16?
pages/api a fost deprecat în Next.js 14 și complet eliminat la nivel de recomandare în Next.js 16. Route Handlers din App Router folosesc API-urile web standard (Request/Response), suportă streaming nativ și se integrează cu Server Components, în timp ce pages/api erau bazate pe Node http și obiectul res stil Express.
Pot folosi Route Handlers cu Edge Runtime?
Da. Adaugă export const runtime = 'edge' în route.ts. Vei avea un cold start sub 50 ms și execuție la marginea CDN-ului, dar pierzi accesul la API-uri Node native. Folosește clienți HTTP precum Neon, PlanetScale sau Drizzle HTTP în loc de driver-e TCP.
De ce primesc params.id ca undefined în Next.js 16?
Începând cu Next.js 15, params este un Promise. Trebuie să faci const { id } = await context.params, nu context.params.id direct. Acest breaking change a fost introdus pentru a permite render parțial al rutei înainte ca parametrii dinamici să fie rezolvați.
Route Handlers sunt cache-uite implicit?
Nu, începând cu Next.js 15. În 13/14 un GET fără API-uri dinamice era cache-uit static la build. Comportamentul a fost inversat în 15/16: implicit dinamic, opt-in pentru cache prin export const dynamic = 'force-static' și export const revalidate = N.
Cum protejez un Route Handler cu autentificare?
Citește cookie-ul de sesiune cu request.cookies.get('session') sau header-ul Authorization cu request.headers.get('authorization'), validează token-ul (JWT, Auth.js session), și returnează 401 dacă nu e valid. Pentru protecție declarativă la nivel de rută folosește proxy.ts sau Auth.js v5 middleware.
Pot avea mai multe metode HTTP în același fișier route.ts?
Da, exportă o funcție per metodă: export async function GET, POST, PATCH, PUT, DELETE, HEAD, OPTIONS. Metodele neexportate primesc automat 405 Method Not Allowed cu header-ul Allow setat corect de Next.js.
Ghid practic pentru Turbopack în Next.js 16: cum activezi build-urile de producție, migrezi de la Webpack, configurezi loadere SVG/GraphQL, filesystem caching în CI și Subresource Integrity nativ. Cu benchmark-uri reale și capcane din producție.
Ghid complet pentru gestionarea erorilor în Next.js 16: error.tsx, not-found.tsx, global-error.tsx, Server Actions și integrare Sentry, cu exemple practice.
Rute paralele și de interceptare în Next.js 16: ghid practic pentru modaluri cu URL partajabil, dashboarduri cu streaming independent și pattern-uri avansate App Router cu exemple de cod complete.