API Routes vs Route Handlers is a question about two routers, not two features. API Routes are the Pages Router way to build an endpoint: a file in pages/api that exports one default handler receiving Node style request and response objects. Route Handlers are the App Router replacement: a route file that exports one function per HTTP method and returns a Web Response.
The difference that drives the whole migration is the response model. An API Route writes to a response object, while a Route Handler returns one.
| API Route | Route Handler | |
|---|---|---|
| Location | pages/api/posts.ts | app/api/posts/route.ts |
| Exports | One default handler | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS |
| Request | NextApiRequest, a Node request | Request or NextRequest, the Web API |
| Response | res.status().json() | return Response.json() |
| Method routing | Branch on req.method | Separate export per method |
| Body parsing | Parsed for you, configurable | Read it yourself from the request |
API Routes continue to work in Next.js 16, so nothing forces a rewrite in one sitting. The two systems can serve different URLs in the same project, which makes an endpoint-by-endpoint migration practical. They cannot both define the same URL, though, so each endpoint needs one cutover.
The before and after
Here is a typical dynamic API Route that only serves GET requests and reads its parameter from the query object.
// pages/api/posts/[pid].ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'GET') {
res.status(405).end()
return
}
res.status(200).json({ id: req.query.pid })
}A request to /api/posts/42 returns the JSON body, and any other method gets the 405 the handler writes by hand.
The App Router version moves the file into a folder named for the dynamic segment, with the endpoint itself named route.ts.
// app/api/posts/[pid]/route.ts
type Ctx = { params: Promise<{ pid: string }> }
export async function GET(_request: Request, ctx: Ctx) {
const { pid } = await ctx.params
return Response.json({ id: pid })
}The URL is unchanged, so callers notice nothing. Three things did change: the method branch became an export name, the parameter moved out of the query object into an awaited params promise, and the status is part of the returned response rather than a call on a response object.
The 405 branch disappeared because Next.js returns that status automatically for any method the file does not export.
Mapping the request and response helpers
Most of the work is mechanical substitution. This table covers what an average endpoint touches.
| Pages Router | App Router |
|---|---|
| req.query.pid on a dynamic route | await params in the second argument |
| req.query.q for a query string | request.nextUrl.searchParams.get('q') |
| req.body | await request.json() or request.formData() |
| req.cookies | request.cookies on NextRequest, or await cookies() from next/headers |
| req.headers | request.headers.get(name) |
| res.status(201).json(data) | Response.json(data, { status: 201 }) |
| res.send(text) | new Response(text) |
| res.redirect(307, '/') | redirect('/') from next/navigation |
| res.revalidate(path) | revalidatePath or revalidateTag |
| config.api.bodyParser = false | Nothing, read request.text() directly |
Query strings and route params are separate concepts in the App Router, where the Pages Router merged both into req.query. Reading each one is covered in reading the request body, query params, and headers.
A body-reading endpoint
Parsing is the second common surprise. An API Route hands you a parsed body, and a Route Handler does not. This small comparison isolates that API change, so keep the original endpoint's schema validation and authorization when you migrate it.
// pages/api/subscribe.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { email } = req.body
res.status(201).json({ email })
}The body arrives already parsed according to the content type, which is convenient until you need the raw bytes for a signature check and have to disable the parser through the config export.
// app/api/subscribe/route.ts
export async function POST(request: Request) {
const { email } = await request.json()
return Response.json({ email }, { status: 201 })
}You choose the parsing method yourself, and the raw text is always available. That removes the bodyParser configuration entirely, which is why webhook endpoints are simpler in the App Router.
The body can only be read once per request, so clone the request before either consumer reads it if two pieces of code both need it.
Behavior differences to plan for
Beyond syntax, a few defaults are genuinely different, and these are the ones that produce surprises after the file compiles.
- Method handling. An unsupported method returns 405 without any code from you.
- Caching. Route Handlers are not cached by default. GET handlers changed from static to dynamic in Next.js 15, and opting back in with a force-static route config belongs to the pre-Cache-Components model described in caching without Cache Components. With Cache Components enabled, a GET handler can be prerendered when it touches no uncached or runtime data.
- Static export. API Routes cannot be used with a static export, while GET Route Handlers can with a force-static config.
- Route conflicts. A route file and a page file cannot sit at the same App Router route. An App Router file and a Pages Router file also cannot resolve to the same URL.
Neither system adds cross-origin headers for you, so an endpoint called from another origin still needs its own configuration in both routers.
Doing the migration
Next.js does not list an official codemod for this conversion, so move each endpoint manually. It does ship codemods for neighbouring upgrades, such as renaming middleware to proxy and making request APIs asynchronous, so run those separately if they apply.
A practical order for each endpoint:
- Extract shared business logic so both handler shapes can be tested without duplicating it.
- Build the Route Handler under a temporary, non-conflicting URL and compare its status, headers, and body with the old endpoint.
- Split method branches into exports, return Response objects, and await params.
- In one cutover change, delete the pages/api file and move route.ts to the folder that owns the original URL.
- Run the endpoint checks again at the original URL before deploying.
The most common error while doing this is reading a property off the params object without awaiting it, which is covered in fixing "params should be awaited".
The short version
Move the file into an app folder as route.ts, export one function per method, return a Response instead of writing to one, and await the params promise. Nothing forces you to convert every endpoint at once, so migrate the ones you are already editing first. The target convention is explained in route handlers and route.js.
Rune AI
Key Insights
- API Routes live in pages/api and export one default handler.
- Route Handlers live in an app folder as route.ts and export one function per method.
- The res helpers are replaced by returning a Response object.
- Route params arrive as a promise in the second argument and must be awaited.
- Unsupported methods return 405 automatically instead of needing a branch.
- Both routers can coexist, but two files cannot own the same URL.
Frequently Asked Questions
Is there a codemod for this migration?
Can pages/api and app route files coexist?
Do API Routes still work in Next.js 16?
What happened to the bodyParser config?
Conclusion
Migrating an API Route means moving the file into an app folder as route.ts, splitting the method branches into named exports, and swapping the Node style request and response objects for the Web Request and Response APIs. There is no codemod, but the mapping is mechanical, and pages/api keeps working while you convert endpoints one at a time.
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.