Your frontend fires POST /api/checkout. The Network panel shows 405 Method Not Allowed. The handler you swear you wrote never logs a line.
In the Pages Router era this usually meant a missing if (req.method === 'POST') branch. In the App Router it almost always means something quieter: Next.js never registered a POST handler for that path, so it correctly refuses the method before your code runs.
Here is the trap, four lookalikes that produce the same status code, and a short checklist that ends the guessing.
The trap: wrong export shape
App Router Route Handlers live in app/api/.../route.ts and register methods by uppercase named exports:
// app/api/checkout/route.ts
export async function POST(request: Request) {
const body = await request.json();
return Response.json({ ok: true }, { status: 201 });
}
These do not register POST:
// ❌ default export — Pages Router muscle memory
export default async function handler(req: Request) { /* ... */ }
// ❌ lowercase — not a recognized method export
export async function post(request: Request) { /* ... */ }
// ❌ method switch inside a default export
export default async function handler(req: any, res: any) {
if (req.method === 'POST') { /* ... */ }
}
Next.js supports GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. If the matched route.ts does not export the method the client sent, you get 405 — by design.
Four lookalikes that feel like "my POST is broken"
1. Wrong file path
POST /api/checkout only hits app/api/checkout/route.ts. A file at app/api/checkout/route.tsx, pages/api/checkout.ts, or app/api/checkouts/route.ts will not help. Dynamic segments must match too: /api/tickets/12 needs app/api/tickets/[id]/route.ts.
2. page.tsx and route.ts at the same segment
You cannot put both at the same route segment. If a page.tsx already owns /api/checkout (unusual but it happens during messy migrations), the Route Handler never mounts the way you expect. Split the paths.
3. Proxy / middleware short-circuit
Next.js has been moving from middleware.ts toward a proxy convention. Whatever sits in front of the handler can return, redirect, or rewrite before your POST export runs. If logs inside POST never fire, inspect the proxy/middleware matcher and early returns next — not the handler body.
4. CORS preflight confusion
A browser cross-origin POST often starts with OPTIONS. If you only exported POST and a gateway or edge layer answers OPTIONS poorly, DevTools can look like "POST failed" when the preflight did. For intentional cross-origin APIs, export OPTIONS (or set headers in config) and verify with a real preflight. A local CORS header tester helps isolate whether the browser is blocked before your handler runs: CORS Header Tester & Generator.
A five-minute triage
- Confirm the request URL and method in DevTools (no trailing-slash surprise, no rewrite).
- Open the exact
app/api/.../route.tsthat should own that path. - Confirm an uppercase
export async function POST(or whichever method you need). - Add one log on the first line of that export. Redeploy / refresh. If the log never appears, the export is not matched — fix path or export shape before debugging business logic.
- If only production fails, check the deployment adapter, basePath, and any CDN rules that strip or rewrite
/api.
Migration note (Pages → App)
You can keep pages/api endpoints alive while you migrate UI routes. When you do move an endpoint, rewrite it as named method exports on the Web Request/Response APIs — do not paste a Pages-style default handler into route.ts and expect it to work. For the broader migration checklist (layouts, navigation hooks, async params), see the Next.js App Router vs Pages Router migration guide on Blogs World.
If you are stuck on a stubborn 405 after the export shape is correct, the deeper walkthrough — Proxy matchers, deployment adapters, and App Router-specific pitfalls — is here: Next.js API Route 405 Method Not Allowed Fix (App Router).
Bottom line
A 405 in App Router Route Handlers is usually not a mystery HTTP bug. It means no handler was registered for that method on that path. Fix the export name and the file location first. Everything else is a lookalike.
— Published for developers who would rather spend the afternoon on product work than on route.ts archaeology. More practical guides and browser tools: Blogs World.
Top comments (0)