Skip to content

Fix: Express Cannot GET /route (404 Not Found)

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix Express.js Cannot GET route 404 error caused by wrong route paths, missing middleware, route order issues, static files, and router mounting problems.

The Error

You visit a route in your Express app and get:

Cannot GET /api/users

Or:

<!DOCTYPE html>
<html>
<head><title>Error</title></head>
<body><pre>Cannot GET /api/users</pre></body>
</html>

With HTTP status 404 Not Found.

Or for POST requests:

Cannot POST /api/users

Express does not have a route handler registered for the requested URL and HTTP method. The request falls through all route handlers and Express responds with its default 404 message.

Why This Happens

Express matches incoming requests against registered routes in order. Each route registered with app.get, app.post, or router.use is appended to a stack. When a request comes in, Express walks the stack and invokes the first handler whose method and path pattern match. If no handler matches, Express falls through to its built-in 404 sender, which writes the literal string Cannot GET /path (or Cannot POST, etc.) to the response.

The message is misleading because it suggests the URL itself is wrong. In practice the URL is usually fine — what is wrong is one of the surrounding pieces: the router was never mounted, the path on the router is relative to a mount prefix you forgot about, a middleware higher up swallowed the request, or the request never reached this Express process at all because a reverse proxy rewrote the path. Treat “Cannot GET” as “no handler matched after applying every middleware and prefix above this point” and the debugging path becomes clearer.

Common causes:

  • Route path typo. /api/user registered but requesting /api/users.
  • Wrong HTTP method. Route uses app.post() but the request is a GET.
  • Route order issue. A catch-all route or middleware above intercepts the request.
  • Router not mounted. A router module is defined but never app.use()’d.
  • Missing leading slash. api/users instead of /api/users.
  • Case sensitivity. Express routes are case-insensitive by default, but path params are not.
  • Trailing slash mismatch. /api/users/ vs /api/users.
  • Wrong host or interface. The server is listening on IPv6 ::1 and you are hitting it on IPv4 127.0.0.1, so the request reaches a different process (or nothing at all).
  • Reverse proxy path rewrite. Nginx or Caddy strips or rewrites the prefix before forwarding, and the route Express sees is not the route you typed in the browser.

Platform and Environment Differences

The same “Cannot GET” 404 has very different causes depending on where the app runs.

Linux and Docker (case-sensitive filesystem). Linux ext4, the default for most Docker base images, is case-sensitive. If you register app.get('/api/Users', handler) and request /api/users, Express itself is case-insensitive so this usually still matches — but the moment you serve static files with express.static('./Public'), the case of the directory matters. A teammate who developed on macOS may have committed Public/ while production looks for public/, and every request 404s in production but works locally. Always lowercase your directory names and verify with ls -la on a Linux host before deploying.

macOS (APFS, case-insensitive by default). APFS is case-insensitive but case-preserving. app.use(express.static('Public')) works whether the actual folder is Public/ or public/. This hides bugs that surface only after a deploy to Linux. If you want to catch these locally, format a development volume as case-sensitive APFS via Disk Utility and check out your repo there.

Windows native (NTFS, case-insensitive). NTFS is also case-insensitive by default, so a Windows developer cannot reproduce the Linux case-sensitivity bug. Beyond that, Windows has its own quirk: when you app.listen(3000) without a host argument, Node binds to :: (all IPv6) and the IPv6/IPv4 dual-stack behavior depends on the Windows TCP stack settings. If localhost in your browser resolves to ::1 but a non-dual-stack listener bound to 0.0.0.0 is running, the request never reaches Express and you get a connection refused rather than the 404 — but if a different process is listening on ::1:3000, you can get a “Cannot GET” from that other process and waste an hour blaming Express.

WSL2. Listen on 0.0.0.0, not localhost, otherwise Windows cannot forward traffic into the WSL2 distro. app.listen(3000, '0.0.0.0') is the safe form. If you hit http://localhost:3000/api/users from Windows and get “Cannot GET”, first verify the request actually reached the WSL2 process by tailing your access log; the 404 may be coming from a stale port-proxy entry left behind by an earlier netsh interface portproxy configuration.

Docker. Container networking adds two layers. First, the container must EXPOSE and -p 3000:3000 must map the port. Second, the app must bind to 0.0.0.0 inside the container — binding to 127.0.0.1 means only the container itself can reach the port, and the host gets connection refused. The 404 specifically appears when the request does reach Express but the route is missing, which inside a container often means you forgot to COPY a routes directory or node_modules was not rebuilt for the container’s architecture.

Production behind nginx / Caddy / a load balancer. This is the single most common cause of “works locally, 404 in production.” Nginx with location /api/ { proxy_pass http://localhost:3000/; } (note the trailing slash on proxy_pass) strips /api before forwarding, so Express sees /users, not /api/users. Without the trailing slash, nginx forwards the path verbatim. The mismatch between dev (http://localhost:3000/api/users → Express sees /api/users) and prod (https://example.com/api/users → Express sees /users) produces 404s only in production. Always test with console.log(req.path) in a wildcard handler to see what Express actually receives.

Serverless (Vercel, Netlify Functions, AWS Lambda with serverless-express). The function URL prefix is stripped by the platform before reaching Express. A function at /api/server.js gets requests with the leading /api already removed. If your Express app has app.get('/api/users', ...), you will get 404 in production but a working route locally where you ran node server.js directly.

Fix 1: Check the Route Path

Verify the registered route matches exactly:

Broken — typo in route:

// Registered: /api/user (singular)
app.get('/api/user', (req, res) => {
  res.json({ users: [] });
});

// Request: /api/users (plural) → Cannot GET /api/users

Fixed:

app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

Debug — list all registered routes:

// Print all routes after setup
app._router.stack.forEach((r) => {
  if (r.route) {
    console.log(`${Object.keys(r.route.methods).join(',')} ${r.route.path}`);
  }
});

Or use express-list-endpoints:

npm install express-list-endpoints
const listEndpoints = require('express-list-endpoints');
console.log(listEndpoints(app));

Pro Tip: When you get “Cannot GET”, first check the exact URL you are requesting against the exact routes registered. A single character difference (trailing slash, plural vs singular, typo) causes a 404.

Fix 2: Fix Route Method Mismatch

The HTTP method must match:

// Only responds to POST
app.post('/api/users', (req, res) => {
  res.json({ created: true });
});

// GET /api/users → Cannot GET /api/users
// POST /api/users → { created: true }

Handle multiple methods:

app.route('/api/users')
  .get((req, res) => {
    res.json({ users: [] });
  })
  .post((req, res) => {
    res.json({ created: true });
  });

Or use app.all() for any method:

app.all('/api/health', (req, res) => {
  res.json({ status: 'ok' });
});

Fix 3: Fix Router Mounting

If you define routes in a separate router file, you must mount it:

Broken — router defined but not mounted:

// routes/users.js
const router = require('express').Router();

router.get('/', (req, res) => {
  res.json({ users: [] });
});

module.exports = router;

// app.js
const userRoutes = require('./routes/users');
// Forgot to mount! userRoutes is imported but never used

Fixed — mount the router:

// app.js
const userRoutes = require('./routes/users');
app.use('/api/users', userRoutes);  // Mount at /api/users

Route prefix matters:

// routes/users.js
router.get('/', ...);       // Matches /api/users
router.get('/:id', ...);    // Matches /api/users/123
router.post('/', ...);      // Matches POST /api/users

// app.js
app.use('/api/users', userRoutes);  // Prefix: /api/users

The router’s paths are relative to the mount point. router.get('/') + app.use('/api/users', router) = /api/users.

Common Mistake: Defining the full path in both the router and the mount point. router.get('/api/users') mounted at app.use('/api', router) creates /api/api/users, not /api/users.

Fix 4: Fix Route Order

Express matches routes in the order they are defined. Earlier matches win:

Broken — catch-all before specific route:

// This catches everything!
app.get('*', (req, res) => {
  res.sendFile('index.html');
});

// This never runs
app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

Fixed — specific routes before catch-all:

// API routes first
app.get('/api/users', (req, res) => {
  res.json({ users: [] });
});

// Catch-all for SPA routing last
app.get('*', (req, res) => {
  res.sendFile('index.html');
});

Broken — static files middleware before routes:

app.use(express.static('public'));
// If public/api/users/index.html exists, it will be served instead of the route
app.get('/api/users', handler);

Fixed — mount API routes before static files:

app.use('/api', apiRouter);
app.use(express.static('public'));

Fix 5: Fix Static File Serving

If you are trying to serve static files (HTML, CSS, JS):

const path = require('path');

// Serve files from the 'public' directory
app.use(express.static(path.join(__dirname, 'public')));

// File at public/index.html → http://localhost:3000/index.html
// File at public/css/style.css → http://localhost:3000/css/style.css

For SPA (React, Vue, Angular) with client-side routing:

// API routes
app.use('/api', apiRouter);

// Static files
app.use(express.static(path.join(__dirname, 'build')));

// SPA fallback — must be LAST
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

Fix 6: Fix Trailing Slash Issues

/api/users and /api/users/ are different routes by default:

app.get('/api/users', handler);
// GET /api/users → 200
// GET /api/users/ → 301 redirect to /api/users (default Express behavior)

Enable strict routing to treat them as different:

app.set('strict routing', true);
// Now /api/users and /api/users/ are different routes

Or handle both explicitly:

app.get('/api/users/?', handler);  // Regex: trailing slash optional

Fix 7: Fix Body Parsing Middleware

Missing body parser causes POST/PUT handlers to fail silently:

// Must be before route definitions!
app.use(express.json());                         // Parse JSON bodies
app.use(express.urlencoded({ extended: true }));  // Parse form bodies

// Now POST routes can access req.body
app.post('/api/users', (req, res) => {
  console.log(req.body);  // Without express.json(), this is undefined
  res.json({ created: true });
});

Fix 8: Add a Custom 404 Handler

Replace Express’s default “Cannot GET” response:

// Define all routes first...

// 404 handler — must be after all routes
app.use((req, res, next) => {
  res.status(404).json({
    error: 'Not Found',
    message: `Route ${req.method} ${req.url} not found`,
    timestamp: new Date().toISOString(),
  });
});

// Error handler — must be after 404 handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({
    error: 'Internal Server Error',
    message: err.message,
  });
});

A custom 404 handler is also the fastest way to debug “Cannot GET” in production. Log req.method, req.originalUrl, and req.headers['x-forwarded-prefix'] (if you sit behind a proxy) so the next 404 tells you exactly what Express received. Compare that against the browser URL — the gap between them points straight at the misconfigured proxy or mount prefix.

Still Not Working?

Check that the server is running on the expected port:

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

Check for proxy issues. If using Nginx or a reverse proxy, the proxy path might not match:

# Nginx strips /api prefix if not configured correctly
location /api/ {
    proxy_pass http://localhost:3000/api/;  # Include trailing slash on both
}

Check for CORS preflight failures. OPTIONS requests return 404 if not handled:

const cors = require('cors');
app.use(cors());  // Handles OPTIONS preflight requests

Check the bound interface. If app.listen(3000) works on macOS but returns “Cannot GET” or connection refused on Windows / WSL2, bind explicitly to 0.0.0.0:

app.listen(3000, '0.0.0.0', () => {
  console.log('Listening on 0.0.0.0:3000');
});

This forces IPv4 and avoids the dual-stack :: vs 127.0.0.1 mismatch that bites Windows and containerized setups.

Check the request actually reaches Express. Add a wildcard logger at the very top:

app.use((req, res, next) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.originalUrl}`);
  next();
});

If you never see the log line for the failing request, the request is being intercepted earlier — by a reverse proxy, a service worker in the browser, a CDN, or a different process bound to the same port. The 404 is coming from somewhere other than your Express app.

Check for trailing whitespace in environment variables. If your route path comes from process.env.API_PREFIX and the .env file accidentally has a trailing space, you register /api/users (with a space) and requests to /api/users 404. Trim explicitly: (process.env.API_PREFIX || '').trim().

For Node.js module errors, see Fix: Node.js ERR_MODULE_NOT_FOUND. For port already in use, see Fix: Port 3000 already in use. For CORS errors, see Fix: CORS Access-Control-Allow-Origin error. For other Express middleware misconfigurations that produce confusing 404s and empty bodies, see Fix: Express req.body undefined.

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