Route Handlers v Next.js 16: Kompletní průvodce REST API v App Router

Praktický průvodce Route Handlers v Next.js 16. Stavte REST API v App Router – cache, dynamické routy, validace pomocí Zod, autentizace, streamování a nasazení na Vercel Edge Runtime.

Route Handlers Next.js 16: REST API 2026

Route Handlers v Next.js 16 jsou moderní náhradou starých pages/api souborů – a upřímně, jakmile si na ně zvyknete, zpátky se vám fakt nechce. Pokud pracujete s App Routerem, jsou jedinou oficiální cestou, jak vystavit HTTP endpointy přímo z vaší Next.js aplikace. Tenhle průvodce vás provede vším od první minimální syntaxe až po produkční nasazení s cache, validací a autentizací, s důrazem na novinky, které přinesla verze 16.

Co jsou Route Handlers a kdy je použít

Route Handler je jednoduše soubor route.ts (nebo route.js, pokud TypeScript neřešíte) umístěný uvnitř složky app/. Exportuje HTTP metody jako funkce: GET, POST, PUT, PATCH, DELETE, HEAD a OPTIONS. Na rozdíl od Server Actions, které se volají primárně z formulářů, jsou Route Handlers určené pro klasické REST API – mobilní klienty, externí webhooky, integrace třetích stran nebo prostě veřejné JSON endpointy.

Route Handlers vs. Server Actions

  • Route Handlers – veřejné HTTP endpointy, libovolná metoda, vhodné pro externí klienty.
  • Server Actions – RPC-styl volání mezi klientskou komponentou a serverem, integrace s formuláři, automatická revalidace.
  • Server Components – přímé volání databáze bez nutnosti vystavovat endpoint.

Tady přiznám osobní názor: pokud data potřebujete pouze ve vlastní aplikaci, je často rychlejší (a bezpečnější) sáhnout po Server Components nebo Server Actions. Route Handlery dávají smysl tam, kde potřebujete skutečný HTTP kontrakt – tedy něco, na co se dá zavolat zvenčí.

Minimální Route Handler v Next.js 16

Tak, pojďme do toho. Vytvořte soubor app/api/hello/route.ts:

import { NextRequest, NextResponse } from "next/server";

export async function GET(request: NextRequest) {
  return NextResponse.json({ message: "Ahoj z Next.js 16!" });
}

A je to. Endpoint je okamžitě dostupný na /api/hello. Žádná další konfigurace, žádný custom server, nic. Návratová hodnota je standardní Response objekt z Web Fetch API – NextResponse je jen tenká vrstva nad ním s pohodlnými helpery.

Cache chování v Next.js 16: hlavní změna

Tohle je oblast, kde se Next.js 16 zásadně liší od starších verzí (a kde mě v minulosti několikrát překvapil v produkci, takže pozor). Ve verzích 13 a 14 byly GET handlery defaultně cachované. Od Next.js 15 jsou všechny Route Handlers defaultně dynamické – cache si musíte explicitně vyžádat. Verze 16 tenhle model dále zpřesnila pomocí direktivy "use cache" a Cache Components.

Explicitní cache pomocí "use cache"

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

export async function GET() {
  "use cache";

  const products = await fetch("https://api.example.com/products", {
    next: { revalidate: 3600 },
  }).then((r) => r.json());

  return NextResponse.json(products);
}

Direktiva "use cache" v Next.js 16 nahrazuje dřívější segment config export const dynamic = "force-static" a, řekněme si to upřímně, je výrazně srozumitelnější. Pokud potřebujete revalidovat manuálně, zkombinujte ji s tagy:

import { unstable_cacheTag as cacheTag } from "next/cache";

export async function GET() {
  "use cache";
  cacheTag("products");

  const data = await db.product.findMany();
  return Response.json(data);
}

Pak stačí v Server Action zavolat revalidateTag("products") – a cache se invaliduje napříč celou aplikací. Hezké, ne?

Dynamické segmenty a parametry

Stejně jako stránky, i Route Handlers podporují dynamické segmenty pomocí složek typu [id]. V Next.js 16 jsou ale params asynchronní – nezapomeňte na await (mně to v prvním projektu trvalo dlouho, než jsem si zvykl):

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

export async function GET(
  request: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const product = await db.product.findUnique({ where: { id } });

  if (!product) {
    return NextResponse.json({ error: "Nenalezeno" }, { status: 404 });
  }
  return NextResponse.json(product);
}

Query string získáte přes request.nextUrl.searchParams:

export async function GET(request: NextRequest) {
  const limit = Number(request.nextUrl.searchParams.get("limit") ?? 10);
  const offset = Number(request.nextUrl.searchParams.get("offset") ?? 0);
  const items = await db.product.findMany({ take: limit, skip: offset });
  return NextResponse.json(items);
}

Validace vstupu pomocí Zod

Pravidlo číslo jedna: nikdy nedůvěřujte tělu requestu. Vážně, nikdy. V Next.js 16 je doporučeným postupem parsovat a validovat každý vstup pomocí Zod (případně Valibot, pokud chcete menší bundle):

import { z } from "zod";
import { NextRequest, NextResponse } from "next/server";

const CreateProductSchema = z.object({
  name: z.string().min(1).max(120),
  price: z.number().int().positive(),
  category: z.enum(["clothing", "electronics", "books"]),
});

export async function POST(request: NextRequest) {
  const json = await request.json();
  const result = CreateProductSchema.safeParse(json);

  if (!result.success) {
    return NextResponse.json(
      { errors: result.error.flatten() },
      { status: 422 }
    );
  }

  const product = await db.product.create({ data: result.data });
  return NextResponse.json(product, { status: 201 });
}

Tento vzor – safeParse + 422 odpověď s detailem chyb – kopíruje konvenci běžnou v Laravel a NestJS a klientům se s ní pracuje fakt pohodlně.

Autentizace Route Handlerů

Existují tři běžné scénáře. Pro session cookies (typicky Auth.js v5) ověřte session přímo:

import { auth } from "@/lib/auth";

export async function GET() {
  const session = await auth();
  if (!session?.user) {
    return Response.json({ error: "Neautorizováno" }, { status: 401 });
  }
  // dále pracujte se session.user.id
}

Pro Bearer tokeny (mobilní klienti, server-to-server):

export async function POST(request: NextRequest) {
  const header = request.headers.get("authorization");
  const token = header?.replace(/^Bearer\s+/i, "");

  if (!token || !(await verifyJwt(token))) {
    return NextResponse.json({ error: "Invalid token" }, { status: 401 });
  }
  // ...
}

A pro webhooky (Stripe, GitHub) – tady pozor – ověřujte HMAC podpis nad raw tělem, nikdy ne po volání request.json():

export async function POST(request: NextRequest) {
  const raw = await request.text();
  const signature = request.headers.get("stripe-signature")!;
  const event = stripe.webhooks.constructEvent(raw, signature, process.env.STRIPE_WEBHOOK_SECRET!);
  // ...
  return new Response(null, { status: 204 });
}

Streamování odpovědí

Route Handlers v Next.js 16 mohou vracet ReadableStream, což je ideální pro LLM proxy, server-sent events nebo prostě velké exporty:

export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder();
      for (let i = 0; i < 5; i++) {
        controller.enqueue(encoder.encode(`data: ${i}\n\n`));
        await new Promise((r) => setTimeout(r, 500));
      }
      controller.close();
    },
  });

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

Edge Runtime vs. Node.js Runtime

Každý Route Handler může běžet buď v Node.js runtime (default) nebo na Edge:

export const runtime = "edge"; // nebo "nodejs"

Edge Runtime má v polovině roku 2026 cold start kolem 50 ms oproti 300–800 ms u Node.js a běží globálně blízko uživatele. Není to ale zadarmo – cenou je omezené API: žádný fs, žádné nativní moduly, žádný klasický Prisma klient (potřebujete Prisma Accelerate nebo Drizzle s HTTP driverem). Pro geo-routing, A/B testy a lehké API je Edge skvělá volba. Pro práci s databází přes TCP nebo náročnější logiku raději zůstaňte u Node.js.

CORS a OPTIONS

Pokud vaše API volá frontend z jiné domény, musíte explicitně obsloužit preflight:

const CORS = {
  "Access-Control-Allow-Origin": "https://app.example.com",
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
  "Access-Control-Allow-Headers": "Content-Type, Authorization",
};

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

export async function GET() {
  return NextResponse.json({ ok: true }, { headers: CORS });
}

Hvězdičku v Allow-Origin používejte vážně jen u skutečně veřejných API. Pro autentizované endpointy whitelistujte konkrétní domény, jinak si koledujete o problém.

Rate limiting v praxi

Veřejné endpointy musí mít rate limit – jinak vás první škrabák jednoduše položí. Nejjednodušší přístup je s Upstash Redis:

import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const limiter = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(20, "1 m"),
});

export async function POST(request: NextRequest) {
  const ip = request.headers.get("x-forwarded-for") ?? "anon";
  const { success, remaining } = await limiter.limit(ip);

  if (!success) {
    return NextResponse.json({ error: "Too many requests" }, { status: 429 });
  }
  // ...
}

Chybové stavy a observabilita

Drsná pravda produkce: nejvíc bolestí nepřinášejí 200 OK, ale neošetřené chyby. Obalte tělo handleru do try/catch, logujte se strukturovaným kontextem a vraťte konzistentní tvar chyby:

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    // ...
    return NextResponse.json(result);
  } catch (error) {
    console.error("[products:create]", { error, url: request.url });
    return NextResponse.json(
      { error: "Interní chyba serveru" },
      { status: 500 }
    );
  }
}

Na Vercelu logy automaticky proudí do dashboardu. V self-hostu napojte výstup na Sentry, Axiom nebo OpenTelemetry exportér – cokoliv z toho je lepší než hádat, co se právě stalo.

Časté chyby a jak se jim vyhnout

  • Zapomenutý await params – v Next.js 16 jsou params Promise. TypeScript vám to napoví, ale runtime chyba je tichá.
  • Vrácení NextResponse.json pro velká data – serializace blokuje event loop. Pro odpovědi nad ~5 MB raději streamujte.
  • Spoléhání na request.ip – v Edge a za reverzní proxy je null. Vždy preferujte x-forwarded-for.
  • Cache na endpointech závislých na cookies – tichý data leak. Takové handlery vždy nechte dynamické.
  • Smíchání Route Handler a Server Action ve stejné cestě – Server Action v souboru page.tsx nepoužívejte jako veřejné API; cesta totiž není stabilní.

Nasazení a testování

Na Vercelu Route Handlers fungují bez další konfigurace – výchozí runtime je Node.js, regiony si můžete omezit přes export const preferredRegion = ["fra1", "iad1"]. Pro self-host (Docker, AWS) běží stejný build, jen si pohlídejte, aby váš reverzní proxy nepřepisoval Host a x-forwarded-* hlavičky.

Integrační testy nejjednodušeji napíšete pomocí vitest a přímého volání exportované funkce:

import { GET } from "@/app/api/products/route";

it("vrací seznam produktů", async () => {
  const res = await GET(new Request("http://localhost/api/products"));
  expect(res.status).toBe(200);
  const data = await res.json();
  expect(Array.isArray(data)).toBe(true);
});

Často kladené otázky

Jaký je rozdíl mezi Route Handler a Server Action v Next.js 16?

Route Handler je veřejný HTTP endpoint s libovolnou metodou a URL – volají ho externí klienti i váš frontend přes fetch. Server Action je naproti tomu interní RPC-styl funkce vázaná na komponenty React; volá se přímo z formulářů nebo button handlerů a má automatickou revalidaci. Pokud potřebujete vystavit data mobilní aplikaci nebo třetí straně, použijte Route Handler.

Jsou Route Handlery v Next.js 16 ve výchozím nastavení cachované?

Ne. Od Next.js 15 a stále v Next.js 16 jsou defaultně dynamické. Pokud chcete cache, musíte ji explicitně zapnout pomocí direktivy "use cache" nebo nastavit fetch s next: { revalidate }. Je to obráceně oproti Next.js 13/14, kde byly GET handlery cachované automaticky (a kde se dalo snadno spálit).

Mohu používat Prisma s Edge Runtime?

Klasický Prisma klient běží pouze v Node.js runtime, protože vyžaduje TCP spojení. Pro Edge máte v podstatě tři možnosti: Prisma Accelerate (HTTP proxy nad vaší databází), Prisma Postgres, nebo přechod na Drizzle s HTTP driverem (Neon, PlanetScale, Turso). Pro většinu projektů v roce 2026 mi dává smysl Drizzle + Neon.

Jak v Route Handleru přečtu cookies?

Použijte helper cookies() z next/headers, který je v Next.js 16 asynchronní: const jar = await cookies(); const token = jar.get("session")?.value;. Nastavit cookie můžete buď přes jar.set() nebo přidáním hlavičky Set-Cookie do NextResponse.

Kde Route Handlery běží – na klientovi nebo na serveru?

Vždy na serveru – nikdy se neposílají v bundlu do prohlížeče. To znamená, že v nich můžete bezpečně používat tajné klíče, přímé databázové dotazy a Node.js API (pokud nezapnete Edge Runtime). Z tohoto důvodu jsou ideálním místem pro logiku, kterou klient prostě nesmí vidět.

Shrnutí

Route Handlers v Next.js 16 jsou robustní, plně typovaný nástroj pro stavbu REST API přímo uvnitř vaší aplikace. Klíčové změny oproti starším verzím – asynchronní params, defaultně dynamické chování, direktiva "use cache" a vylepšený Edge Runtime – tlačí vývojáře k explicitnímu, čitelnému kódu. A to je dobrá zpráva. Pokud zkombinujete validaci pomocí Zod, jasnou autentizační strategii a rate limiting, dostanete API, které je rychlé, bezpečné a snadno se škáluje – ať už na Vercelu, nebo v self-hostu.

O Autorovi Tomasz Wieczorek

Tomasz is a senior frontend engineer with eleven years of experience, currently a tech lead at a Berlin-based fintech where he migrated 180 routes from CRA to Next.js 14 App Router over nine months without a single user-facing outage. Before that, four years at Klarna building the merchant dashboard in Next.js, and two years at a small agency in Krakow shipping marketing sites for European retailers. He maintains two small open-source libraries in the React ecosystem, one for typed environment variables and one for a Suspense-friendly data fetching wrapper that predates the React 19 use() hook. He prefers Drizzle over Prisma, NextAuth over Clerk for self-hosted setups, and has strong feelings about putting business logic in Server Components. He writes the kind of tutorials that show the broken intermediate state, not just the working final code.