Skip to content

Fix: Next.js API route 404 Not Found or not responding

FixDevs ·

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 found

Or 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 failed
Internal Server Error (500) from API route

Your 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:

  1. Pages Routerpages/api/ directory with default exports.
  2. App Routerapp/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.

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/ and app/api/ define the same route.
  • Middleware blocking the route. A middleware.ts file is intercepting the request.
  • Deployment configuration. The serverless function is not deployed correctly.

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/:id

Correct 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 ← Correct

Pro Tip: In the App Router, the file MUST be named route.ts (or route.js). Not index.ts, not handler.ts, not page.ts. Only route.ts is 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/:id

Correct 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.ts

Do 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);
}

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].ts

Common 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).*)',
  ],
};

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 --follow

Static 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 detected

Environment 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 dev

Check for TypeScript errors that prevent compilation:

npx tsc --noEmit

Check the Next.js version. App Router API routes require Next.js 13.2+. Route Handlers (route.ts) were introduced in 13.2.

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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles