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.
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 thisexport 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 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.
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.
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.
Before
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();}
After
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);}
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. ↩
With a few extra things specific to Next.js. But we are not using the pages router here so it doesn't matter. ↩