Fix: Next.js API route 404 Not Found or not responding
Part of: React & Frontend Errors
Quick Answer
How to fix Next.js API route 404 not found errors caused by wrong file paths, App Router vs Pages Router confusion, incorrect exports, and deployment issues.
The Error
You create a Next.js API route and get:
404 - This page could not be foundOr when calling the API:
GET http://localhost:3000/api/users 404 (Not Found)POST http://localhost:3000/api/users 405 (Method Not Allowed)TypeError: fetch failedInternal Server Error (500) from API routeYour API endpoint exists in the codebase but Next.js does not serve it. The route either cannot be found, does not export the right handler, or has a configuration issue.
Why This Happens
Next.js supports API routes in two different systems:
- Pages Router —
pages/api/directory with default exports. - App Router —
app/api/directory with named HTTP method exports (GET,POST, etc.).
Mixing conventions between the two systems is the most common cause of API route failures.
The two routers share the same URL space but resolve files in different ways. The App Router walks app/ looking for route.ts files and registers each named HTTP-verb export as a handler; anything else (a default export, a lowercased verb, a page.ts file in the same folder) is silently ignored or interpreted as a page. The Pages Router walks pages/api/ and treats the default export of any .ts/.js file as the request handler regardless of which verbs it supports. If both routers define the same path, the App Router wins — but the file that loses is not deleted, only shadowed, which makes the bug look like a stale build cache.
A second class of failure comes from the runtime. The App Router supports two runtimes: Node.js (the default) and the Edge runtime (export const runtime = 'edge'). The Edge runtime is a different JavaScript environment — no fs, no process.env outside the build-time bundle, no native modules, a smaller Web-standard surface. A route that works in Node and then gets pinned to edge for cold-start latency will start throwing 500s the moment it touches anything Node-specific. The error shows up as a generic 500 in the browser and a stack trace in the deploy logs that says “X is not available in Edge runtime.”
Common causes:
- Wrong directory. Using
app/api/syntax in a Pages Router project or vice versa. - Wrong export. App Router needs named exports (
GET,POST), Pages Router needs a default export. - File naming error. Missing
route.ts(App Router) or wrong file extension. - Conflicting routes. Both
pages/api/andapp/api/define the same route. - Middleware blocking the route. A
middleware.tsfile is intercepting the request. - Deployment configuration. The serverless function is not deployed correctly.
In Production: Incident Lens
In production this typically shows up immediately after a deploy as a per-route outage. The blast radius is bounded — only requests to the affected path fail — but if the broken route is on a hot path (auth callback, payments webhook, session refresh), the user-visible damage scales fast. The classic incident is App Router vs Pages Router collision: a developer added an App Router app/api/checkout/route.ts while the existing pages/api/checkout.ts was still serving production traffic. Locally next dev happily served the new file, on Vercel the build promoted the new route, and the old handler — which had subtle differences in body parsing, status codes, or third-party signature verification — was silently retired. The result is POST /api/checkout 500 from production while staging is green because staging tested through a different code path.
The monitoring signal is a synthetic check against every API route that runs as part of the post-deploy verification job, not just an uptime probe against /. The probe should send a representative request (real payload shape, real auth header) and assert both the status code and a content invariant from the response body. CloudWatch, Datadog Synthetics, BetterStack, or even a curl | jq step in the CD pipeline is enough. Pair this with a structured log query that alerts on route ... compiled warnings or Cannot find module errors during the build phase — those messages are your earliest warning that an API surface changed.
The recovery sequence depends on the deploy platform. On Vercel, instant rollback to the previous deployment is the right move because Vercel keeps the previous build live and switchable; do that first, then debug. On a self-hosted Next.js instance behind an ALB, you usually do not have a one-click rollback, so cut a fix that explicitly removes the conflicting file (git rm app/api/checkout/route.ts or pages/api/checkout.ts) and redeploy. The postmortem preventive is a route-inventory test in CI: a single Jest or Vitest test that imports your Next.js route manifest and asserts every expected path is reachable with the expected verb, plus a lint rule that forbids the same logical path existing in both app/api/ and pages/api/.
Fix 1: Fix App Router API Routes (Next.js 13+)
App Router API routes use route.ts files with named HTTP method exports:
Correct file structure:
app/
api/
users/
route.ts ← /api/users
users/
[id]/
route.ts ← /api/users/:idCorrect exports:
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(request: NextRequest) {
const users = await getUsers();
return NextResponse.json(users);
}
export async function POST(request: NextRequest) {
const body = await request.json();
const user = await createUser(body);
return NextResponse.json(user, { status: 201 });
}Common mistakes:
// WRONG — default export (this is Pages Router syntax)
export default function handler(req, res) {
res.json({ hello: "world" });
}
// WRONG — lowercase method name
export async function get(request) { ... } // Must be GET
// WRONG — file named page.ts instead of route.ts
// app/api/users/page.ts ← This creates a page, not an API route!
// app/api/users/route.ts ← CorrectPro Tip: In the App Router, the file MUST be named
route.ts(orroute.js). Notindex.ts, nothandler.ts, notpage.ts. Onlyroute.tsis recognized as an API route handler.
Fix 2: Fix Pages Router API Routes
Pages Router API routes use default exports in pages/api/:
Correct file structure:
pages/
api/
users.ts ← /api/users
users/
index.ts ← /api/users (alternative)
[id].ts ← /api/users/:idCorrect exports:
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
const users = getUsers();
res.status(200).json(users);
} else if (req.method === 'POST') {
const user = createUser(req.body);
res.status(201).json(user);
} else {
res.setHeader('Allow', ['GET', 'POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}Common mistakes:
// WRONG — named export instead of default
export function handler(req, res) { ... } // Must be default export
// WRONG — async without proper error handling
export default async function handler(req, res) {
const data = await fetch("https://api.example.com"); // Unhandled rejection if this fails
}
// FIXED — with error handling
export default async function handler(req, res) {
try {
const response = await fetch("https://api.example.com");
const data = await response.json();
res.status(200).json(data);
} catch (error) {
res.status(500).json({ error: "Internal server error" });
}
}Fix 3: Fix Route Conflicts Between Routers
If your project has both app/ and pages/ directories:
app/
api/
users/
route.ts ← App Router /api/users
pages/
api/
users.ts ← Pages Router /api/users (CONFLICT!)Next.js gives App Router priority, but this can cause confusing behavior.
Fix: Use only one router for API routes:
# Option 1: All API routes in App Router
app/
api/
users/route.ts
posts/route.ts
# Option 2: All API routes in Pages Router
pages/
api/
users.ts
posts.tsDo not mix. Pick one approach and use it consistently.
Fix 4: Fix Dynamic Route Parameters
App Router dynamic params:
// app/api/users/[id]/route.ts
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const user = await getUser(id);
if (!user) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json(user);
}In Next.js 15+, the params object is a Promise and must be awaited. In earlier versions it was a plain object.
Pages Router dynamic params:
// pages/api/users/[id].ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { id } = req.query; // id is string | string[]
const userId = Array.isArray(id) ? id[0] : id;
// ...
}Catch-all routes:
// App Router: app/api/[...slug]/route.ts
// Pages Router: pages/api/[...slug].tsCommon Mistake: Forgetting that dynamic route parameters in the App Router are accessed differently than in the Pages Router. App Router uses the second function argument; Pages Router uses
req.query.
Fix 5: Fix Request Body Parsing
App Router — parse body manually:
// app/api/users/route.ts
export async function POST(request: NextRequest) {
const body = await request.json(); // Must await .json()
// body is now the parsed JSON
// For FormData:
const formData = await request.formData();
// For text:
const text = await request.text();
}Pages Router — body is auto-parsed:
// pages/api/users.ts
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const body = req.body; // Already parsed (if Content-Type is application/json)
}Disable body parsing in Pages Router (for file uploads):
export const config = {
api: {
bodyParser: false,
},
};
export default async function handler(req, res) {
// Handle raw body manually
}Fix 6: Fix Middleware Interference
A middleware.ts file might be blocking API routes:
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
// This might redirect API routes!
if (!request.cookies.get('session')) {
return NextResponse.redirect(new URL('/login', request.url));
}
}
// Fix: Exclude API routes from middleware
export const config = {
matcher: [
// Match all paths except API routes and static files
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
};Middleware must live at the project root (or src/ root), not inside app/ or pages/. A misplaced middleware.ts simply does not run.
Fix 7: Fix CORS Issues
API routes called from a different origin need CORS headers:
App Router:
export async function GET(request: NextRequest) {
const data = await getData();
return NextResponse.json(data, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}
// Handle preflight requests
export async function OPTIONS() {
return new NextResponse(null, {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
},
});
}Pages Router:
export default function handler(req, res) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}
// Handle the actual request
}Fix 8: Fix Deployment Issues
Vercel — check serverless function logs:
vercel logs --followStatic export does not support API routes:
// next.config.js
module.exports = {
output: 'export', // API routes will NOT work with static export!
};If you use output: 'export', API routes are not available. Use a server-based deployment or remove the output option.
Check the build output:
npm run build
# Look for API routes in the output:
# ƒ /api/users (dynamic)
# If your route is not listed, it was not detectedEnvironment variables not available:
// .env.local
DATABASE_URL=postgresql://localhost:5432/mydb
// Access in API routes:
const url = process.env.DATABASE_URL; // Works in API routes (server-side)API routes run on the server, so they can access all process.env variables, not just NEXT_PUBLIC_* ones.
Still Not Working?
Restart the dev server. New API route files sometimes are not detected until you restart:
# Kill the dev server and restart
npm run devCheck for TypeScript errors that prevent compilation:
npx tsc --noEmitCheck the Next.js version. App Router API routes require Next.js 13.2+. Route Handlers (route.ts) were introduced in 13.2.
Check the runtime declaration. If you have export const runtime = 'edge' at the top of a route.ts that imports fs, path, crypto (Node version), or a native dependency, the build will succeed but the runtime call will throw. Remove the line to fall back to the Node runtime, or refactor the route to use Web-standard APIs (crypto.subtle, fetch, Response).
Check next.config.js for rewrites or redirects. A rewrite that maps /api/:path* to an external origin silently bypasses your local handler. A redirect that fires on /api/* turns every request into a 307 the client did not expect. Search next.config.js for rewrites(), redirects(), and headers() and confirm none of them shadow your route.
Check route caching on Vercel. App Router routes are cached by default for GET requests. If you see stale data from an API route, add export const dynamic = 'force-dynamic' or export const revalidate = 0. If you want fine-grained caching, see Fix: Next.js App Router fetch cache for the cache directives.
Reproduce against the production URL with curl -v. The browser’s network tab hides important details (CORS preflight rewrites, redirect chains, CDN cache hits). A verbose curl shows the exact response headers and confirms whether the request even reaches your function or is being short-circuited at the edge.
For Next.js hydration errors, see Fix: Next.js hydration failed. For module resolution errors in Next.js, see Fix: Next.js Module not found: Can’t resolve ‘fs’. For general CORS issues, see Fix: CORS Access-Control-Allow-Origin error.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: React Hydration Error — Text Content Does Not Match
How to fix React hydration errors — server/client HTML mismatches, useEffect for client-only code, suppressHydrationWarning, dynamic content, and Next.js specific hydration issues.
Fix: Next.js App Router Fetch Not Caching or Always Stale
How to fix Next.js App Router fetch caching issues — understanding cache behavior, revalidation with next.revalidate, opting out with no-store, cache tags, and debugging stale data.
Fix: Next.js CORS Error on API Routes
How to fix CORS errors in Next.js API routes — adding Access-Control headers, handling preflight OPTIONS requests, configuring next.config.js headers, and avoiding common proxy mistakes.
Fix: Next.js Middleware Not Running (middleware.ts Not Intercepting Requests)
How to fix Next.js middleware not executing — wrong file location, matcher config errors, middleware not intercepting API routes, and how to debug middleware execution in Next.js 13 and 14.