---
title: "10 — API Endpoints — Route Handlers, Static vs Dynamic, Streaming, Redirects"
uid: api-endpoints
tags: ["roadmap:nextjs", "streaming", "route-handlers", "api", "redirects", "nextjs"]
excerpt: "In the App Router, route handlers live in route.ts and use web-standard Request/Response — static or dynamic, streaming, redirects, all in one convention."
date: 2026-08-13T03:28:01+0000
source: https://www.aveshina.my.id/en/blog/api-endpoints
---

Building HTTP APIs inside Next.js used to mean remembering which convention era I was in. The model that clicked: **in the App Router, API endpoints live in route.ts files anywhere under app/, use web-standard Request/Response objects, and cover everything from JSON handlers to streaming to redirects in one convention** [1][2]. The Pages Router's pages/api/* was the older approach; the App Router's route handlers are the modern default.

## Route handlers — the App Router way

In the App Router, a route.ts (or route.js) file inside any app/ subdirectory defines an API endpoint [1]. The file exports named functions for the HTTP methods it handles, each receiving a standard web Request and returning a Response:

```
// app/api/posts/route.ts
export async function GET(request: Request) {
  const posts = await fetchPosts();
  return Response.json(posts);
}

export async function POST(request: Request) {
  const body = await request.json();
  const post = await createPost(body);
  return Response.json(post, { status: 201 });
}
```

The shift from Pages Router is deliberate: instead of Express-like req/res objects, route handlers use the **web standard Request/Response API** — the same one browsers use [2]. This makes them portable to any web-standard runtime (Edge included) and aligns the way of thinking with the platform.

## Static vs dynamic endpoints

Route handlers split into two flavors based on how they're defined and what they read [3]:

- **Static endpoints** have predefined routes and typically return the same response for every request — cacheable, often prerendered at build time.
- **Dynamic endpoints** use parameters (app/api/posts/[id]/route.ts) and generate responses based on those parameters — per-request work.

The App Router infers which is which from the code: if the handler reads request-time data (search params, cookies, headers), it's dynamic; otherwise it can be statically prerendered.

## Catch-all segments

When I don't know the exact route segment names ahead of time, **catch-all segments** ([...slug]) extend an API route to match all subsequent paths in one handler [4]. A handler at app/api/[...path]/route.ts receives every path under /api/* and can route internally. This is the building block for proxy endpoints, CMS webhooks, and any case where the URL shape is variable.

## Streaming responses

Streaming lets me send data to the client in chunks rather than waiting for the entire response to be generated on the server first [5]. For long-running processes or large datasets, this dramatically improves perceived performance — the client starts processing and displaying information sooner. A streaming route handler returns a ReadableStream:

```
// app/api/stream/route.ts
export async function GET() {
  const stream = new ReadableStream({
    async start(controller) {
      for (const chunk of generateChunks()) {
        controller.enqueue(new TextEncoder().encode(chunk));
        await delay(100);
      }
      controller.close();
    },
  });
  return new Response(stream);
}
```

This is the same Suspense-driven streaming concept from the UI side, applied to raw API responses. The roadmap is explicit about the payoff: incremental delivery makes long processes feel responsive [5].

## Redirects

API endpoints can also redirect by returning the appropriate HTTP response, instructing the browser to navigate to a new URL [6]. The framework provides a redirect() helper, and direct Response returns with a 3xx status work too:

```
import { redirect } from 'next/navigation';

export async function GET() {
  redirect('/new-location');
}
```

Redirects are useful for moved or renamed resources, temporary changes, or routing users based on conditions (auth state, locale) [6].

```figure
<svg viewBox="0 0 720 300" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Anatomy of an App Router route handler. A route.ts file with exported GET and POST functions, each taking a Request and returning a Response. Arrows show: GET returning static JSON (cached), POST returning dynamic JSON, a streaming branch sending chunks, and a redirect branch returning a 3xx.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <rect x="40" y="40" width="240" height="220" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="160" y="62" font-size="12" font-weight="700" fill="#1e1b4b" text-anchor="middle">app/api/.../route.ts</text>
    <g font-family="ui-monospace, monospace" font-size="10" fill="#1e1b4b">
      <text x="60" y="88">export GET(req)</text>
      <text x="60" y="106">export POST(req)</text>
    </g>
    <text x="160" y="138" font-size="10" font-style="italic" fill="#1e1b4b" text-anchor="middle">web-standard Request</text>

    <!-- outcomes -->
    <line x1="280" y1="80" x2="420" y2="80" stroke="#16a34a" stroke-width="1.5"/>
    <rect x="420" y="64" width="180" height="32" rx="6" fill="#dcfce7" stroke="#16a34a" stroke-width="1"/>
    <text x="510" y="84" font-size="10" fill="#052e16" text-anchor="middle">static JSON (cached)</text>

    <line x1="280" y1="120" x2="420" y2="120" stroke="#ca8a04" stroke-width="1.5"/>
    <rect x="420" y="104" width="180" height="32" rx="6" fill="#fef9c3" stroke="#ca8a04" stroke-width="1"/>
    <text x="510" y="124" font-size="10" fill="#422006" text-anchor="middle">dynamic JSON</text>

    <line x1="280" y1="160" x2="420" y2="160" stroke="#6366f1" stroke-width="1.5"/>
    <rect x="420" y="144" width="180" height="32" rx="6" fill="#e0e7ff" stroke="#6366f1" stroke-width="1"/>
    <text x="510" y="164" font-size="10" fill="#1e1b4b" text-anchor="middle">streaming chunks</text>

    <line x1="280" y1="200" x2="420" y2="200" stroke="#dc2626" stroke-width="1.5"/>
    <rect x="420" y="184" width="180" height="32" rx="6" fill="#fee2e2" stroke="#dc2626" stroke-width="1"/>
    <text x="510" y="204" font-size="10" fill="#7f1d1d" text-anchor="middle">redirect (3xx)</text>

    <text x="360" y="280" font-size="11" font-style="italic" fill="#64748b" text-anchor="middle">one convention, four response shapes</text>
  </g>
</svg>
```

## How I use this

Route handlers are my default for any HTTP API the app needs internally — webhooks, third-party integrations, lightweight endpoints. The web-standard Request/Response model means I think the same way on Node and Edge. For static-ish data (a config endpoint, a rarely-changing list), I let it prerender or cache. For real-time or per-user data, I mark it dynamic. For long-running work, I reach for streaming. And for moved resources, a redirect is a one-liner.

## References

[1] Vercel, "Route handlers and middleware," Next.js Docs, 2024. [Online]. Available: [https://nextjs.org/docs/app/getting-started/route-handlers-and-middleware](https://nextjs.org/docs/app/getting-started/route-handlers-and-middleware)

[2] Vercel, "Building APIs with Next.js," Next.js Blog, 2024. [Online]. Available: [https://nextjs.org/blog/building-apis-with-nextjs](https://nextjs.org/blog/building-apis-with-nextjs)

[3] Vercel, "Building APIs with Next.js — App Router vs Pages Router," Next.js Blog, 2024. [Online]. Available: [https://nextjs.org/blog/building-apis-with-nextjs#12-app-router-vs-pages-router](https://nextjs.org/blog/building-apis-with-nextjs#12-app-router-vs-pages-router)

[4] Vercel, "Catch-all segments," Next.js Docs, 2024. [Online]. Available: [https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes#catch-all-segments](https://nextjs.org/docs/app/api-reference/file-conventions/dynamic-routes#catch-all-segments)

[5] Vercel, "Streaming (route handlers)," Next.js Docs, 2024. [Online]. Available: [https://nextjs.org/docs/app/api-reference/file-conventions/route#streaming](https://nextjs.org/docs/app/api-reference/file-conventions/route#streaming)

[6] Vercel, "How to handle redirects in Next.js (App Router)," Next.js Docs, 2024. [Online]. Available: [https://nextjs.org/docs/app/guides/redirecting](https://nextjs.org/docs/app/guides/redirecting)

[7] "Next.js 15 tutorial — Route handlers," YouTube, 2024. [Video]. Available: [https://www.youtube.com/watch?v=27Uj6BeIDV0](https://www.youtube.com/watch?v=27Uj6BeIDV0)

```quiz
Q: In the App Router, where do API endpoints live?
- pages/api/*.js
- route.ts files anywhere under app/
correct: 1
explain: The App Router uses route.ts (or route.js) files inside app/ subdirectories, exporting named functions per HTTP method. pages/api/* was the Pages Router convention.

Q: Route handlers use which request/response objects?
- Express-style req/res
- Web-standard Request/Response
correct: 1
explain: App Router route handlers use the web-standard Request and Response objects, the same ones browsers use. This makes them portable across runtimes including Edge.

Q: What distinguishes a static API endpoint from a dynamic one in the App Router?
- Static uses GET; dynamic uses POST
- The compiler infers it — if the handler reads request-time data (search params, cookies, headers), it's dynamic; otherwise it can be statically prerendered
correct: 1
explain: The App Router infers static vs dynamic from the code. Handlers that read no request-time data can be prerendered or cached; those that do render dynamically per request.

Q: A catch-all segment ([...slug]) in a route.ts file lets you…
- match all subsequent paths in a single handler
- only handle OPTIONS requests
correct: 0
explain: Catch-all segments extend a route to match all subsequent paths, useful for proxies, CMS webhooks, and variable URL shapes handled in one place.

Q: Streaming a route handler response is useful because…
- it reduces the total bytes sent
- the client starts processing and displaying chunks sooner, before the whole response is generated
correct: 1
explain: Streaming sends data incrementally, improving perceived performance for long-running processes or large datasets. The client doesn't wait for the entire response before seeing output.
```
