Fix NotFoundError: Cannot GET /api/users/profile in Express
This error occurs when no route matches the requested path and method in Express, resulting in a default 404 HTML response. This usually means the route is misspelled, uses the wrong HTTP method, or the router is not mounted at the correct path. Fix it by adding a catch-all 404 handler and verifying your route definitions.
Reading the Stack Trace
Here's what each line means:
- Cannot GET /api/users/profile: Express could not find a route matching GET /api/users/profile and returned its default HTML 404 page.
- at /app/node_modules/express/lib/router/index.js:284:15: The Express router exhausted all registered routes without finding a match, falling through to the default 404 handler.
- at trim_prefix (/app/node_modules/express/lib/router/index.js:328:13): Express is trimming the route prefix while searching for a matching route handler in the router stack.
Common Causes
1. Misspelled route path
The route is defined with a different path than what the client requests, causing no match.
const router = express.Router();
// Route defined as /user/profile (singular)
router.get('/user/profile', (req, res) => {
res.json({ user: req.user });
});
// Client requests /users/profile (plural) — no match
2. Router not mounted at the correct prefix
The router is mounted at a different prefix than expected, so the full path does not match the client request.
const usersRouter = require('./routes/users');
// Mounted at /user instead of /api/users
app.use('/user', usersRouter);
// Client requests /api/users/profile — no match
3. No catch-all 404 handler
The application does not define a fallback handler for unmatched routes, so Express returns its default HTML error page.
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
// No 404 handler — Express sends default HTML error
app.listen(3000);
The Fix
Add a catch-all middleware after all route definitions that returns a JSON 404 response with the requested method and URL. This replaces Express's default HTML error page and helps API consumers understand what went wrong.
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
app.listen(3000);
const usersRouter = express.Router();
usersRouter.get('/', (req, res) => res.json({ users: [] }));
usersRouter.get('/profile', (req, res) => res.json({ user: req.user }));
app.use('/api/users', usersRouter);
// Catch-all 404 handler — MUST be after all routes
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Cannot ${req.method} ${req.originalUrl}`,
availableEndpoints: ['/api/users', '/api/users/profile']
});
});
app.listen(3000);
Testing the Fix
const request = require('supertest');
const express = require('express');
function createApp() {
const app = express();
const router = express.Router();
router.get('/', (req, res) => res.json({ users: [] }));
router.get('/profile', (req, res) => res.json({ user: { id: 1 } }));
app.use('/api/users', router);
app.use((req, res) => {
res.status(404).json({ error: 'Not Found', message: `Cannot ${req.method} ${req.originalUrl}` });
});
return app;
}
describe('Route matching', () => {
it('returns 200 for existing routes', async () => {
const res = await request(createApp()).get('/api/users');
expect(res.status).toBe(200);
});
it('returns 200 for /api/users/profile', async () => {
const res = await request(createApp()).get('/api/users/profile');
expect(res.status).toBe(200);
expect(res.body.user.id).toBe(1);
});
it('returns JSON 404 for non-existent routes', async () => {
const res = await request(createApp()).get('/api/nonexistent');
expect(res.status).toBe(404);
expect(res.body.error).toBe('Not Found');
expect(res.body.message).toBe('Cannot GET /api/nonexistent');
});
});
Run your tests:
npx jest --testPathPattern=route-not-found
Pushing Through CI/CD
git checkout -b fix/express-route-not-found,git add src/app.js src/routes/users.js src/__tests__/routeNotFound.test.js,git commit -m "fix: add JSON 404 catch-all handler for unmatched routes",git push origin fix/express-route-not-found
Your CI config should look something like this:
name: CI
on:
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npx jest --coverage
- run: npm run lint
The Full Manual Process: 18 Steps
Here's every step you just went through to fix this one bug:
- Notice the error alert or see it in your monitoring tool
- Open the error dashboard and read the stack trace
- Identify the file and line number from the stack trace
- Open your IDE and navigate to the file
- Read the surrounding code to understand context
- Reproduce the error locally
- Identify the root cause
- Write the fix
- Run the test suite locally
- Fix any failing tests
- Write new tests covering the edge case
- Run the full test suite again
- Create a new git branch
- Commit and push your changes
- Open a pull request
- Wait for code review
- Merge and deploy to production
- Monitor production to confirm the error is resolved
Total time: 30-60 minutes. For one bug.
Or Let bugstack Fix It in Under 2 minutes
Every step above? bugstack does it automatically.
Step 1: Install the SDK
npm install bugstack-sdk
Step 2: Initialize
const { initBugStack } = require('bugstack-sdk')
initBugStack({ apiKey: process.env.BUGSTACK_API_KEY })
Step 3: There is no step 3.
bugstack handles everything from here:
- Captures the stack trace and request context
- Pulls the relevant source files from your GitHub repo
- Analyzes the error and understands the code context
- Generates a minimal, verified fix
- Runs your existing test suite
- Pushes through your CI/CD pipeline
- Deploys to production (or opens a PR for review)
Time from error to fix deployed: Under 2 minutes.
Human involvement: zero.
Try bugstack Free →No credit card. 5-minute setup. Cancel anytime.
Deploying the Fix (Manual Path)
- Run the test suite locally to confirm all routes resolve and unknown routes return JSON 404.
- Open a pull request with the route and 404 handler changes.
- Wait for CI checks to pass on the PR.
- Have a teammate review and approve the PR.
- Merge to main and verify route matching in staging before promoting to production.
Frequently Asked Questions
BugStack tests all registered routes for correct responses and verifies that unmatched routes return JSON 404s instead of HTML before marking it safe to deploy.
Every fix is delivered as a pull request with full CI validation. Your team reviews and approves before anything reaches production.
Express was originally designed for server-rendered web apps that serve HTML. For JSON APIs, you must add a custom 404 handler to return JSON responses instead of the default HTML error page.
The 404 catch-all handler should go after all routes but before any error-handling middleware (4-argument functions). This way unmatched routes get a 404, and errors from matched routes get proper error handling.