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 Next.js 16 Ghid (2026)

Actualizat: 10 iunie 2026

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:

function withErrorHandler(
  handler: (req: NextRequest) => Promise<Response>
) {
  return async (req: NextRequest) => {
    try {
      return await handler(req)
    } catch (err) {
      console.error('[API]', err)
      return NextResponse.json(
        { error: 'Internal Server Error' },
        { status: 500 }
      )
    }
  }
}

export const POST = withErrorHandler(async (req) => {
  // logica ta
  return NextResponse.json({ ok: true })
})

Autentificare cu cookies și headers

Route Handlers au acces direct la cookies și headers prin NextRequest:

// app/api/me/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { verifyJwt } from '@/lib/auth'

export async function GET(request: NextRequest) {
  const token = request.cookies.get('session')?.value
  if (!token) {
    return NextResponse.json(
      { error: 'Unauthorized' },
      { status: 401 }
    )
  }

  const payload = await verifyJwt(token).catch(() => null)
  if (!payload) {
    return NextResponse.json(
      { error: 'Invalid session' },
      { status: 401 }
    )
  }

  return NextResponse.json({ user: payload })
}

Caching, revalidare și runtime dinamic

Î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 RuntimeEdge Runtime
Cold start~200–500 ms< 50 ms
Memorie maximă1–3 GB (Vercel)~128 MB
API-uri Node native (fs, child_process)DaNu
Driver-e DB TCP (pg, mysql2, mongodb)DaNu (doar HTTP: Neon, PlanetScale, Drizzle HTTP)
Streaming nativDaDa
Maximum execution timepână la 300s (Pro)30s (request) / 25s (background)
Cel mai bun pentruDB queries, logică grea, integrări SDKAuth, 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:

// app/api/chat/route.ts
export const runtime = 'edge'

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

  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder()
      const tokens = ['Salut', ', ', 'cum', ' pot', ' ajuta', '?']

      for (const token of tokens) {
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ token })}\n\n`)
        )
        await new Promise((r) => setTimeout(r, 80))
      }
      controller.close()
    },
  })

  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
    },
  })
}

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:

const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://app.example.com',
  'Access-Control-Allow-Methods': 'GET, POST, PATCH, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  'Access-Control-Max-Age': '86400',
}

export async function OPTIONS() {
  return new Response(null, { status: 204, headers: corsHeaders })
}

export async function POST(request: Request) {
  const body = await request.json()
  return NextResponse.json({ ok: true }, { headers: corsHeaders })
}

Route Handlers vs Server Actions: care când

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.

Editorial Team
Despre Autor Editorial Team

Our team of expert writers and editors.