Ceasefire now! 🕊️🇵🇸
FAQ for the Next.js Discord Server

Why doesn't my route handler work, when it follows the pages router syntax?

I have a route handler at app/.../route.js that looks like this,

export function POST(req, res) {
  const { name } = req.body;
  res.json({ message: `Hello, ${name}` });

But it doesn't work. Why?

The syntax used in the pages router API routes (the syntax that you used) and the syntax used in the app router route handlers are entirely different.

This is important, so I have to repeat it again.

The syntax used in the pages router API routes (the syntax that you used) and the syntax used in the app router route handlers are entirely different.

If you want to use route handlers in the app router, you need to migrate your code to the new syntax. Check the official documentation for detailed information. Summarised below are some of the major differences that I have seen many people confused about.

You now export named functions instead of a generic handler

In the app router, you no longer export default function handler() which handles all HTTP methods. You now export named functions for each HTTP method you want to handle.

For example, if you want to support POST and PUT methods, you would export two functions, POST and PUT.

// Don't do this, it doesn't work
// export default function handler() {}
// Instead do this
export function POST() {
  // Handle POST requests
export function PUT() {
  // Handle PUT requests

and all other methods will get a 405 Method Not Allowed response. In the example above, GET, DELETE, PATCH, etc., will all get a 405 response.

If you want to use the same function for more than one method, you can do so by exporting the same function for multiple methods. This technique is used in some places, such as next-auth.

function handlePostAndPut() {
  // Handle POST and PUT requests
export { handlePostAndPut as POST, handlePostAndPut as PUT };

The function signature is different

It is no longer (req, res). It is just (req) now.1

And the old req and the new req are nowhere near the same. Read on...

The types are different


The old req is based on the Node.js http.IncomingMessage type.2 That sounds like a lot of jargon, but basically speaking, it's the same req as the req you use in Express.js.

The new req is based on the native Request type.3 Basically, the same req as you use in middleware and the browser's Fetch API.

This means there are a few differences:

Old reqNew req
Read JSON body4req.bodyawait req.json()5
Read string bodyreq.body
(bodyParser disabled)
await req.text()6
Read FormData bodyThird party packages
such as multer
await req.formData()
Read HTTP headersreq.headers.authorizationreq.headers.get("authorization")
or use headers()
Read cookiesreq.cookiesreq.cookies.get("cookieName")
or use cookies()
Read HTTP methodreq.methodreq.method
though do you need it?
Read the
request pathname
I don't remember 🥲,7
maybe req.url
Read dynamic
segment parameters
req.queryThe context parameter
Read search parameters
(query values)
I don't remember 🥲,
maybe req.query


The old res is based on the Node.js http.ServerResponse type. It's the same res as the res you use in Express.js.

Now you no longer have a res parameter. Instead, you return a response object. The response object is based on the native Response type. It's the same res as you use in the browser's Fetch API.

Hence, there are also a few differences:

Old resNew method
Send JSON responseres.json({ message: "Hello, world" })return Response.json({ message: "Hello, world" })
Send string responseres.send("Hello, world")return new Response("Hello, world")
Set status coderes.status(403)return new Response(..., { status: 403 })
return Response.json({...}, { status: 403 })
Set headersres.setHeader("x-hello", "world")return new Response(..., { headers: { "x-hello": "world" } })
return Response.json({...}, { headers: { "x-hello": "world" } })8
Set cookiesres.cookie("cookieName", "cookieValue")cookies().set("cookieName", "cookieValue")
Redirectres.redirect(...)return Response.redirect(...) or redirect()


As you can see, there are so many differences between the old and new syntax. It's not just a matter of changing the function signature. You have to change the way you read the request body, the way you send the response, and even the way you set headers and cookies.

So don't expect that you can simply move your files from pages/api to app and everything will work. Quite likely, it won't.

So time to migrate the code to the new syntax. Or just keep using the pages router, it still works fine and is still supported.

Good luck.

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== "POST") return res.status(405).json({ error: "Method Not Allowed" });
  const result = validate(req.body, schema);
  if (!result.success) return res.status(400).json({ error: result.error });
  const { email, password } = req.body;
  const user = await getUserByEmail(email);
  if (!user || !checkPassword(user, password))
    return res.status(401).json({ error: "Invalid email or password" });
  const token = createToken(user);
  res.cookie("token", token, { httpOnly: true });
  return res.status(200).end();
export function POST(req: NextRequest) {
  const body = await req.json();
  const result = validate(body, schema);
  if (!result.success) return Response.json({ error: result.error }, { status: 400 });
  const { email, password } = body;
  const user = await getUserByEmail(email);
  if (!user || !checkPassword(user, password))
    return Response.json({ error: "Invalid email or password" }, { status: 401 });
  const token = createToken(user);
  cookies().set("token", token, { httpOnly: true });
  return new Response(null);


  1. Well, to be more precise, it is (req, ctx), with the context parameter used to get the dynamic segment params value. You can check the documentation for more information.

  2. With a few extra things specific to Next.js. But we are not using the pages router here so it doesn't matter.

  3. With a few extra things specific to Next.js. Refer to the documentation of NextRequest for more information.

  4. Note that some HTTP methods, such as GET, don't have a body.

  5. Does it look familiar? Yesss! You do await res.json() after you fetch something in the browser. It's the same syntax here.

  6. By the way, you can get string response in the browser with await res.text() too. Now you know.

  7. Sorry, haven't used the pages router or Express.js for a long time.

  8. Note that Response.json automatically sets the Content-Type header to application/json, so you don't need to do that again.

This site is NOT an official Next.js or Vercel website. Learn more.

On this page