API Routes vs Route Handlers: Migrating from Pages Router

What changes when a pages/api handler becomes an app route file, a mapping table for every request and response helper, and the behavior differences to expect.

9 min read

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 RouteRoute Handler
Locationpages/api/posts.tsapp/api/posts/route.ts
ExportsOne default handlerGET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
RequestNextApiRequest, a Node requestRequest or NextRequest, the Web API
Responseres.status().json()return Response.json()
Method routingBranch on req.methodSeparate export per method
Body parsingParsed for you, configurableRead 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.

typescripttypescript
// 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.

typescripttypescript
// 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 RouterApp Router
req.query.pid on a dynamic routeawait params in the second argument
req.query.q for a query stringrequest.nextUrl.searchParams.get('q')
req.bodyawait request.json() or request.formData()
req.cookiesrequest.cookies on NextRequest, or await cookies() from next/headers
req.headersrequest.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 = falseNothing, 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.

typescripttypescript
// 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.

typescripttypescript
// 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

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.
RunePowered by Rune AI

Frequently Asked Questions

Is there a codemod for this migration?

No official codemod is listed for converting API Routes to Route Handlers. Next.js ships codemods for other upgrades, such as renaming middleware to proxy and making request APIs async, but this conversion changes both the handler signature and response model.

Can pages/api and app route files coexist?

Yes, when they resolve to different URLs. You can migrate endpoints one at a time, but an API Route and a Route Handler cannot both own the same URL during the cutover.

Do API Routes still work in Next.js 16?

Yes, they continue to work in the pages directory. They are the Pages Router equivalent of Route Handlers, and the App Router documentation points you to route files instead for new work.

What happened to the bodyParser config?

It does not exist in the App Router. Read the unparsed body with request.text(), or use request.arrayBuffer() when a signature library requires bytes.

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.