Next.js 16 Route Handlers: Täydellinen opas REST API:en rakentamiseen App Routerissa

Rakenna tuotantovalmis REST API Next.js 16 Route Handlereilla App Routerissa. Opas kattaa HTTP-metodit, dynaamiset segmentit, Zod-validoinnin, autentikoinnin, striimauksen, rate limitin, välimuistin ja Edge vs Node Runtime -valinnat.

Next.js 16 Route Handlers: REST API -opas (2026)

Päivitetty: 9. heinäkuuta 2026

Next.js 16 Route Handlers ovat App Routerin virallinen tapa rakentaa HTTP-endpointteja. Ne korvaavat vanhan pages/api-hakemiston ja perustuvat suoraan Web Fetch API:n Request- ja Response-objekteihin. Route Handler on route.ts-tiedosto App Routerin sisällä, joka vie ulos HTTP-metodin nimisen funktion (esim. GET, POST). Tässä oppaassa rakennan tuotantovalmiin REST API:n Next.js 16.2:lla. Käyn läpi metodit, dynaamiset segmentit, Zod-validoinnin, autentikoinnin, striimauksen ja välimuististrategiat konkreettisilla, ajettavilla koodiesimerkeillä.

  • Route Handler on app/-hakemistoon sijoitettu route.ts-tiedosto, joka vie ulos yhden tai useamman HTTP-metodin (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS).
  • Next.js 15:sta lähtien dynaamisten segmenttien params on Promise, joka pitää await-purkaa striimaustuen vuoksi.
  • Zodin safeParse palauttaa strukturoidun virheobjektin heittämättä poikkeusta. Se on ainoa oikea tapa validoida pyynnön runko ja parametrit tuotannossa.
  • Käytä Route Handlereita julkisiin API:ihin, webhookeihin ja mobiilikutsuihin. Server Actionit ovat parempi valinta lomakemutaatioihin sovelluksen sisällä.
  • Striimaus onnistuu ReadableStreamilla yhdistettynä force-dynamic-asetukseen. LLM-vastaukset, SSE ja iso raportinlataus toimivat samalla mallilla.
  • Edge Runtime tarjoaa alhaisen latenssin globaaleille reiteille, mutta rajoittaa Node.js-yhteensopivia paketteja. Valitse ajoympäristö per reitti export const runtime -asetuksella.

Mikä on Route Handler Next.js 16:ssa?

Route Handler on Next.js App Routerin natiivi rajapinta, joka toimii sekä palvelin- että edge-ympäristössä ja käyttää suoraan verkkostandardien mukaisia Request- ja Response-tyyppejä. Toisin kuin vanhat pages/api-reitit, Route Handler ei tunne Expressin req/res-signaturea. Se saa Request-objektin ja palauttaa Response-objektin, aivan kuten mikä tahansa moderni Web-työntekijä. Tämä tarkoittaa, että sama koodi toimii Vercelin edge-verkossa, Cloudflare Workersissa tai Node.js-palvelimella ilman muutoksia.

Käytännössä app/api/products/route.ts -tiedosto muuttuu automaattisesti endpointiksi /api/products. Sinun ei tarvitse kirjoittaa reititystaulukoita, ohjaimia tai middleware-kutsujen ketjua. Tiedostojärjestelmä on reititys. Kun sovelluksesi kasvaa, jokainen kansiotasolla lisätty route.ts laajentaa API-pintaa ilman keskitettyä konfiguraatiota. Next.js 16 tuo mukanaan Turbopackin oletusarvoisena kääntäjänä, joten dev-serverillä muutokset päivittyvät alle 100 millisekunnissa myös monimutkaisissa API-tiedostoissa. Katso tarkat sisääntulopisteet Next.js:n virallisesta Route Handlers -dokumentaatiosta.

Route Handlers vs Server Actions: milloin kumpaakin käyttää?

Yleisin virhe, jonka olen nähnyt tiimeissä, on Server Actioneiden käyttäminen kaikkeen, myös silloin kun ulkoinen mobiiliasiakas tai webhook-kutsu tarvitsee saman logiikan. Nyrkkisääntö on aika suoraviivainen: jos ihminen käynnistää toiminnon sovelluksesi käyttöliittymästä, käytä Server Actionia. Jos kone (webhook, cron, mobiilisovellus, kolmas osapuoli) käynnistää sen, käytä Route Handleria. Server Action ei ole julkinen HTTP-endpoint samassa mielessä kuin Route Handler. Sen kutsu on tiukasti sidottu Reactin renderöintimalliin, eikä Stripe voi lähettää webhookia suoraan Server Actioniin.

OminaisuusRoute HandlerServer Action
HTTP-kierrosKyllä, täysi pyyntö/vastausEi, suora funktiokutsu RSC-payloadin kautta
Julkinen API (mobiili, 3. osapuoli)KylläEi
Webhook-yhteensopivuusKylläEi
LomakemutaatiotManuaalinen fetchNatiivi action={fn}
Tyyppiturvallisuus päästä päähänKatkeaa serialisointirajallaSäilyy
Välimuistin invalidointiManuaalinen revalidatePathSisäänrakennettu
Testattavuus PostmanillaKylläEi
Suora HTTP-statuskoodien hallintaKylläRajoitettu

Käytännössä useimmat tuotantosovellukset yhdistävät molemmat: Server Actionit sisäisiin lomakkeisiin ja Route Handlerit julkisiin endpointteihin, webhookeihin ja LLM-striimauksiin. Perusteellisempi esittely mutaatiokuvioista löytyy oppaastamme React Server Components ja Server Actions -suunnittelumallit.

Ensimmäisen Route Handlerin luominen App Routerissa

Aloitetaan yksinkertaisimmalla mahdollisella esimerkillä: endpoint, joka palauttaa tuotelistan JSON-muodossa. Luo tiedosto app/api/products/route.ts ja lisää siihen HTTP-metodifunktio.

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

// Nämä voisivat tulla tietokannasta. Käytämme muistivarastoa esimerkin selkeyden vuoksi.
const products = [
  { id: 1, name: 'Sähköpyörä', price: 1299 },
  { id: 2, name: 'Kaupunkipyörä', price: 549 },
];

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const minPrice = Number(searchParams.get('minPrice') ?? '0');

  const filtered = products.filter((p) => p.price >= minPrice);

  return NextResponse.json(
    { data: filtered, count: filtered.length },
    { status: 200 }
  );
}

Muutama yksityiskohta, joita on syytä huomata. NextResponse on Web-standardin Response-luokan laajennus, joka lisää mukavuusmetodeja kuten NextResponse.json(). Voit palauttaa myös tavallisen Response-objektin; molemmat toimivat identtisesti. request.url on merkkijono, joten kysyparametrien parsimiseen kannattaa käyttää natiivia URL-konstruktoria. Vältä kolmannen osapuolen kirjastoja tähän, Web-API riittää.

HTTP-metodit ja dynaamiset segmentit

Route Handler tukee kaikkia yleisiä HTTP-metodeja: GET, POST, PUT, PATCH, DELETE, HEAD ja OPTIONS. Metodi määritellään yksinkertaisesti viemällä ulos samanniminen funktio isoin kirjaimin. Jos et määrittele OPTIONS-käsittelijää, Next.js luo automaattisesti CORS-preflight-vastauksen sallittujen metodien listalla. Määrittelemättömälle metodille palautetaan automaattisesti 405 Method Not Allowed.

Dynaamiset segmentit toimivat samalla tavalla kuin sivukomponenteissa: kansion nimi [id] sitoo URL-osan parametriksi. Next.js 15:sta lähtien params-objekti on Promise. Tämä oli rikkova muutos, joka mahdollistaa reunapalvelimen aloittavan renderöinnin ennen kuin dynaaminen segmentti on täysin resolvoitu. Poltin itse pari tuntia tähän kun päivitin API-projektin 14:sta 15:een, joten muista await-purkaa se:

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

type Context = { params: Promise<{ id: string }> };

export async function GET(request: Request, { params }: Context) {
  const { id } = await params; // params on Promise Next.js 15+ jälkeen
  const numericId = Number(id);

  if (!Number.isInteger(numericId) || numericId <= 0) {
    return NextResponse.json(
      { error: 'Virheellinen tuotetunniste' },
      { status: 400 }
    );
  }

  const product = await db.product.findUnique({ where: { id: numericId } });
  if (!product) {
    return NextResponse.json({ error: 'Tuotetta ei löytynyt' }, { status: 404 });
  }

  return NextResponse.json({ data: product });
}

export async function DELETE(request: Request, { params }: Context) {
  const { id } = await params;
  await db.product.delete({ where: { id: Number(id) } });
  return new NextResponse(null, { status: 204 });
}

Catch-all-segmentit [...slug] ja optionaaliset catch-all-segmentit [[...slug]] toimivat myös Route Handlereissa. Ne ovat hyödyllisiä esimerkiksi proxy-endpointeissa, jotka välittävät pyyntöjä ylävirran API:lle. Yksityiskohtaiset tiedostosopimukset löydät route.js-tiedostoreferenssistä.

Syötteen validointi Zodilla tuotantoluokkaisesti

TypeScript tarkistaa tyypit kääntöaikana, mutta kun koodi on ajossa selaimen ulottumattomissa, kuka tahansa voi lähettää mielivaltaisen JSON-payloadin. Tuotannossa jokaisen POST-, PUT- ja PATCH-Route Handlerin on validoitava runkonsa Zodilla, tai vastaavalla runtime-validointikirjastolla. Suosittelen safeParse-metodia, joka palauttaa strukturoidun tuloksen heittämättä poikkeusta.

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

const CreateProductSchema = z.object({
  name: z.string().min(2).max(120),
  price: z.number().int().nonnegative(),
  category: z.enum(['bike', 'accessory', 'apparel']),
  tags: z.array(z.string()).max(10).optional(),
});

export async function POST(request: Request) {
  let payload: unknown;
  try {
    payload = await request.json();
  } catch {
    return NextResponse.json(
      { error: 'Virheellinen JSON-runko' },
      { status: 400 }
    );
  }

  const result = CreateProductSchema.safeParse(payload);
  if (!result.success) {
    return NextResponse.json(
      {
        error: 'Validointi epäonnistui',
        // z.treeifyError tuottaa asiakasystävällisen puumaisen virhemuodon
        issues: z.treeifyError(result.error),
      },
      { status: 422 }
    );
  }

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

Kolme kohtaa, jotka pelastavat myöhemmin tunteja tuotanto-ongelmien selvittelyä. Ensinnäkin, kääri request.json()-kutsu try/catchiin, koska rikkinäinen JSON heittää SyntaxErrorin, joka pitää muuntaa 400-vastaukseksi. Toiseksi, käytä 422 Unprocessable Entity -statuskoodia validointivirheille, ei 400:aa. Tämä erottelee syntaksivirheet semanttisista virheistä. Kolmanneksi, z.object() poistaa tuntemattomat kentät oletusarvoisesti, mikä estää yllättävän datan pääsyn tietokantaan. Aika hiljainen mutta tärkeä turvaominaisuus. Zodin virallinen ohje löytyy Zodin dokumentaatiosta.

Autentikointi ja auktorisointi Route Handlereissa

Jokainen ei-julkinen endpoint tarvitsee autentikointitarkistuksen ensimmäisenä toimena. Suosittu kuvio on kääriä autentikointi higher-order-funktioon, joka lukee JWT-tokenin cookie- tai Authorization: Bearer -otsikosta, varmistaa sen ja välittää käyttäjäobjektin varsinaiseen käsittelijään.

// lib/auth.ts
import { NextResponse } from 'next/server';
import { verifyJwt } from './jwt';

export function withAuth<T>(
  handler: (req: Request, user: { id: string; role: string }, ctx: T) => Promise<Response>
) {
  return async (req: Request, ctx: T) => {
    const bearer = req.headers.get('authorization');
    const token = bearer?.replace(/^Bearer /i, '');
    if (!token) {
      return NextResponse.json({ error: 'Kirjautumatta' }, { status: 401 });
    }

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

    return handler(req, user, ctx);
  };
}
// app/api/products/route.ts
import { withAuth } from '@/lib/auth';

export const POST = withAuth(async (req, user) => {
  if (user.role !== 'admin') {
    return NextResponse.json({ error: 'Ei oikeuksia' }, { status: 403 });
  }
  // ... jatka luontia
});

Erottele aina 401 Unauthorized (kirjautumatta) ja 403 Forbidden (kirjautunut, mutta ilman oikeutta). Selainten kehittäjätyökalut, monitorointidashboardit ja mobiiliasiakkaat luokittelevat nämä eri tavalla, joten oikea statuskoodi säästää debugausaikaa. Jos haluat syvempiä autentikointikuvioita, kuten OAuth-integraatiota tai istuntopohjaista autentikointia, katso oppaamme Auth.js v5 ja Next.js 16 autentikointi.

Reaaliaikainen striimaus ReadableStreamillä

Route Handlerit tukevat natiivisti Web-standardien ReadableStream-rajapintaa, mikä tekee LLM-vastausten, palvelinlähetettyjen tapahtumien (SSE) ja suurten tiedostojen striimauksesta melko suoraviivaista. Alla on esimerkki, joka lähettää OpenAI-tyylisiä chunkkeja asiakkaalle rivi kerrallaan.

// app/api/chat/route.ts
export const dynamic = 'force-dynamic'; // ei staattista renderöintiä

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

  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      const words = generateResponse(prompt); // async iterator
      for await (const word of words) {
        const payload = `data: ${JSON.stringify({ word })}\n\n`;
        controller.enqueue(encoder.encode(payload));
      }
      controller.enqueue(encoder.encode('data: [DONE]\n\n'));
      controller.close();
    },
  });

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

Kolme kriittistä asetusta. export const dynamic = 'force-dynamic' estää Next.js:ää yrittämästä staattista renderöintiä, Cache-Control: no-cache estää välimuistiverkkoja puskuroimasta virtaa, ja SSE-muotoinen data: ...-kirjekuorimuoto mahdollistaa asiakkaan EventSource-API:n käytön. Selainpuolella voit kuluttaa striimin joko EventSource-luokalla tai käyttämällä fetch-vastauksen ReadableStream-lukijaa manuaalisesti. Törmäsin viimeksi ihan turhaan Cloudflaren buffer-ongelmaan (30 sekunnin viive ennen ensimmäistä tokenia) juuri siksi että no-transform puuttui otsakkeista.

Rate limiting ja hyökkäysten torjunta

Ilman rate limitiä yksi väärinkäyttäjä voi hakata endpointtiasi tuhannen pyynnön sekunnissa vauhdilla. Serverless-ympäristössä perinteinen muistivarasto ei toimi, koska jokainen kutsu voi osua eri kontti-instanssiin. Suosittu ratkaisu on Upstash Redis, joka tarjoaa HTTP-yhteensopivan rajapinnan ja sopii sekä Edge- että Node-ajoympäristöön.

// lib/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';

export const ratelimit = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 pyyntöä / 10 s
  analytics: true,
  prefix: 'api',
});
// app/api/products/route.ts
import { ratelimit } from '@/lib/rate-limit';
import { NextResponse } from 'next/server';

export async function POST(req: Request) {
  const ip = req.headers.get('x-forwarded-for')?.split(',')[0] ?? '127.0.0.1';
  const { success, limit, remaining, reset } = await ratelimit.limit(ip);

  if (!success) {
    return NextResponse.json(
      { error: 'Liikaa pyyntöjä' },
      {
        status: 429,
        headers: {
          'X-RateLimit-Limit': String(limit),
          'X-RateLimit-Remaining': String(remaining),
          'X-RateLimit-Reset': String(reset),
        },
      }
    );
  }

  // ... jatka
}

Tuotannossa käytä IP-osoitteen sijaan mieluummin kirjautunutta käyttäjä-ID:tä, kun se on saatavilla. IP-pohjainen rajaus voi rangaista koko toimistoa, joka jakaa NAT-osoitteen. Jos endpoint on täysin julkinen, yhdistä molemmat: matalampi rajoitus IP-tasolla ja korkeampi käyttäjätasolla. Tarkempaa keskustelua tietoturvakuvioista löydät oppaastamme Next.js 16 Proxy: autentikointi, tietoturva ja reititys.

Välimuisti ja revalidointi Route Handlereissa

Next.js 16 muutti oletusvälimuistin käyttäytymistä merkittävästi: GET-Route Handlerit ovat oletuksena dynaamisia, eli ne suoritetaan uudelleen jokaisella pyynnöllä. Aikaisemmissa versioissa oletus oli päinvastainen, mikä yllätti monia kehittäjiä puskuroituneilla vastauksilla tuotannossa. Nyt sinun täytyy nimenomaisesti pyytää välimuistia, mikä on turvallisempi oletus mutta vaatii tietoisia valintoja.

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

const getCachedProducts = unstable_cache(
  async () => db.product.findMany(),
  ['products-list'],
  { revalidate: 60, tags: ['products'] }
);

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

Kun mutaatio tapahtuu, esimerkiksi POST-endpoint luo uuden tuotteen, kutsu revalidateTag('products') mitätöimään välimuisti:

import { revalidateTag } from 'next/cache';

export async function POST(req: Request) {
  // ... validoi ja tallenna
  revalidateTag('products'); // seuraava GET hakee tuoreet tiedot
  return NextResponse.json({ data: newProduct }, { status: 201 });
}

Syvempi katsaus välimuistidirektiiveihin ja Next.js 16 Cache Componentsiin löytyy oppaastamme Next.js 16 Cache Components: välimuistin hallinta use cache -direktiivillä.

Edge Runtime vs Node.js Runtime: valinta per reitti

Route Handler voi ajaa joko Node.js- tai Edge-ajoympäristössä. Valinta tehdään reittitasolla viemällä ulos runtime-vakio.

// app/api/geolocation/route.ts
export const runtime = 'edge'; // suositeltu globaaleille alhaisen latenssin endpointeille

export async function GET(req: Request) {
  const country = req.headers.get('x-vercel-ip-country') ?? 'FI';
  return Response.json({ country });
}

Edge Runtime perustuu V8-eristöihin ja käynnistyy noin 10× nopeammin kuin Node.js-serverless-funktio. Se ajaa reunan lähellä käyttäjää, mikä tekee siitä hyvän valinnan geolokaatiolle, A/B-testauksen valinnalle ja API-proxylle. Mutta se ei tue kaikkia Node.js-natiivimoduuleja: fs, net ja moni tietokantajärjestelmän ajuri (esim. suora Postgres-yhteys) eivät toimi. HTTP-pohjaiset tietokannat kuten Supabase, Neon serverless driver ja PlanetScale toimivat hyvin Edgessä.

Usein kysytyt kysymykset

Voiko Route Handleria käyttää samassa hakemistossa page.tsx:n kanssa?

Ei. Tiedostot route.ts ja page.tsx eivät voi olla samassa hakemistossa, koska molemmat vastaavat samasta URL-polusta. Erottele API:t esimerkiksi app/api/*-hakemistoon tai käytä eri reittiä (esim. app/products/route.ts API:lle ja app/products/page/page.tsx UI:lle).

Miten testaan Route Handleria Postmanilla tai curlilla?

Käynnistä next dev, ja Route Handler on käytettävissä osoitteessa http://localhost:3000/api/.... Voit lähettää pyyntöjä millä tahansa HTTP-asiakkaalla samalla tavalla kuin Express-endpointille. Yksikkötestausta varten Route Handler-funktion voi kutsua suoraan Vitestissä välittämällä sille new Request(...) -objektin.

Toimivatko Route Handlerit Edge Runtimessa Vercelin ulkopuolella?

Kyllä. Edge Runtime perustuu Web-standardeihin, joten sama koodi toimii Cloudflare Workersissa, Netlify Edge Functionsissa ja Denossa. Rajoitukset ovat kuitenkin ajoympäristökohtaisia. Esimerkiksi Cloudflare Workersin CPU-aikaraja on tiukempi kuin Vercelin, joten tarkista kohdealustan dokumentaatio ennen tuotantoon vientiä.

Miten välitän tiedostoja Route Handlerin läpi?

Käytä request.formData()-metodia, joka palauttaa FormData-objektin. Tiedostot ovat File-tyyppisiä ja niiden sisältö luetaan arrayBuffer()-metodilla. Suurten tiedostojen (yli 4,5 MB Vercelissä) osalta suositellaan suoraa selain-tallennuspalveluun-latausta (esim. Vercel Blob tai S3 pre-signed URL).

Voinko käyttää samaa Route Handleria REST- ja tRPC-käyttöön?

Kyllä. tRPC:n fetchRequestHandler integroituu Route Handleriin muutamalla rivillä. Luo app/api/trpc/[trpc]/route.ts, joka delegoi kaikki metodit tRPC:n adapterille. Näin saat sekä tyyppiturvallisen tRPC-kutsun sovelluksestasi että perinteisen REST-endpointin ulkoisille asiakkaille.

Editorial Team
Tietoa Kirjoittajasta Editorial Team

Our team of expert writers and editors.