10 — API Endpoints — Route Handlers, Static vs Dynamic, Streaming, Redirects
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].
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
[2] Vercel, "Building APIs with Next.js," Next.js Blog, 2024. [Online]. Available: 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
[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
[5] Vercel, "Streaming (route handlers)," Next.js Docs, 2024. [Online]. Available: 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
[7] "Next.js 15 tutorial — Route handlers," YouTube, 2024. [Video]. Available: https://www.youtube.com/watch?v=27Uj6BeIDV0
Knowledge check · Question 1 of 5
In the App Router, where do API endpoints live?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!