A route handler can return anything the Web Response API can express. The most common shapes are JSON with a status code, a raw file, a stream of content, and a redirect to another URL.
Each shape is just a different return value. Here is the JSON case, which most APIs use.
// app/api/posts/route.ts
export async function GET() {
return Response.json({ posts: [] }, { status: 200 })
}This returns a JSON object containing an empty posts array with a 200 status. The second argument is the standard Response init, where you set the status and any headers.
Return JSON
Both Response.json and NextResponse.json produce a JSON body with the correct content type. Use NextResponse.json when you also need Next.js helpers such as cookies on the same response.
// app/api/posts/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ error: 'Not ready' }, { status: 503 })
}This returns JSON with a 503 status. The same helper accepts 201 for creates or 400 for bad input, so the status travels with the body. Include a status that matches the outcome, such as 201 for a create, so clients do not guess.
Set status codes and headers
Every response can carry a status code and headers. Response.json accepts them as a second argument, and a new Response accepts a status and headers object in its options.
Set the Content-Type explicitly whenever you return anything other than JSON, because clients and crawlers use it to interpret the body. Custom headers such as Cache-Control also travel in the same options object, though the deployment platform may apply its own rules on top.
Return a file
A file is just a Response with an explicit content type. Build the body as text or bytes and set the header that tells the client how to interpret it.
// app/api/report/route.ts
export async function GET() {
const csv = 'name,role\nAda,Engineer\n'
return new Response(csv, {
headers: { 'Content-Type': 'text/csv' },
})
}Visiting /api/report downloads or displays CSV because of the text/csv header. For binary files, pass a Uint8Array or a stream body and set the matching content type. For a downloadable attachment, add a Content-Disposition header so the browser saves the file instead of rendering it.
Return a stream
A ReadableStream lets you send chunks as they are produced. Pass the stream to a new Response and Next.js forwards each chunk to the client.
// app/api/stream/route.ts
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
controller.enqueue(new TextEncoder().encode('first\n'))
await new Promise((resolve) => setTimeout(resolve, 500))
controller.enqueue(new TextEncoder().encode('second\n'))
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
})
}The stream makes first available, waits half a second, then makes second available. The browser, client, proxy, or hosting platform may still buffer chunks, so verify the observed delivery behavior on the platform you deploy to. The dedicated streaming and Server-Sent Events article covers long-lived streams.
Return a redirect
NextResponse.redirect returns a response that sends the client to another URL. Build the target with new URL against the request so the host is preserved.
// app/api/old-posts/route.ts
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
return NextResponse.redirect(new URL('/posts', request.url))
}Visiting /api/old-posts now lands on /posts with a temporary redirect. The redirect function from next/navigation is another Route Handler option.
That function throws to stop execution, returns 307 outside Server Actions, and must be called outside a try block that could catch it. For a permanent move, permanentRedirect returns 308.
Route handlers use the Web Request and Response APIs instead of a framework-specific pair. The same Response objects work across runtimes and can be returned from anywhere in your code. Because NextResponse extends Response, you can mix plain responses and Next.js helpers in the same handler.
Common mistakes
Returning the wrong content type is the most common mistake. If you send JSON from a plain Response without setting the header, clients may treat it as text, so use Response.json or set Content-Type yourself.
Putting the redirect function inside a broad try/catch is another mistake, because the catch can intercept the special error Next.js uses to perform the redirect. Call redirect after the try/catch, or return a NextResponse.redirect response directly.
See NextRequest and NextResponse for the response helpers, and Route Handlers in Next.js for the file convention. To read the input that decides which response to send, see reading the request body, query params, and headers.
Rune AI
Key Insights
- Return JSON with Response.json or NextResponse.json.
- Return files with a new Response and an explicit content type.
- Stream output with a ReadableStream passed to Response.
- Redirect with NextResponse.redirect or the redirect function.
- Pair a status code and headers using the response options argument.
Frequently Asked Questions
Should I use Response.json or NextResponse.json?
How do I set a status code and custom headers?
Does a streaming response work the same on every host?
Conclusion
A route handler can return any Web Response. Use Response.json or NextResponse.json for JSON, a new Response with a content type for files, a ReadableStream for streamed output, and NextResponse.redirect or the redirect function to send the client elsewhere.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.