Menu
CoddyTech

The 404 Handler

Part of the Express Basics section of Coddy's Node.js journey. Lesson 16 of 23.

Express answers an unknown path with its own page, Cannot GET /x, which fits neither your site nor your API.

app.use((req, res) => {
    res.status(404).send('No forecast here. Try /weather/oslo');
});

A middleware with no path runs for every request that reaches it. Put this one after every route and after express.static: requests they answered never get there, so it only sees the ones nothing answered. It answers, so it needs no next.

req.path is the path without the query string, so req.path.startsWith('/api/') tells an API call from a page.

challenge icon

Challenge

Easy

Paths that do not exist get Express's plain Cannot GET page. Replace it with your own: JSON for the API, and a friendly page for visitors.

  1. In server.js, after every route and after express.static, add one last app.use with no path. Requests that something already responded to never reach it, so it sees only the ones nothing handled.
  2. When the path starts with /api/, it responds with status 404 and this JSON: {"error": "Not found"}
  3. For any other path, it responds with status 404 and an HTML page that contains the words Page not found, for example <h1>Page not found</h1>. A link back to the home page is a nice touch.

Try it: Press Run and send /api/nope. You should see status 404 and {"error":"Not found"}. Send /nope: status 404, drawn as a page that says Page not found. Send /api/recipes/1: status 200 and Tomato Soup, which shows your routes still come first.

Try it yourself

import { escapeHtml } from './html.js';
import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';

const app = express();

app.use((req, res, next) => {
    console.log(`${req.method} ${req.url}`);
    next();
});

app.use((req, res, next) => {
    res.set('X-Request-Id', randomUUID());
    next();
});

app.use(express.json());

app.use(express.static('public'));

app.get('/', (req, res) => {
    const items = recipes.map((r) => `<li><a href="/recipes/${r.id}">${escapeHtml(r.title)}</a></li>`);
    res.send(`<link rel="stylesheet" href="/style.css"><h1>Recipe Box</h1><ul>${items.join('')}</ul>`);
});

// type('text') sends plain text, so each title shows on its own line.
app.get('/recipes', (req, res) => {
    const { tag } = req.query;
    const list = tag ? recipes.filter((recipe) => recipe.tags.includes(tag)) : recipes;
    res.type('text').send(list.map((recipe) => recipe.title).join('\n'));
});

app.get('/recipes/:id', (req, res) => {
    const recipe = recipes.find((r) => r.id === Number(req.params.id));
    if (!recipe) {
        res.status(404).send('No such recipe');
        return;
    }
    res.send(recipe.title);
});

app.get('/api/recipes', (req, res) => {
    res.json(recipes);
});

app.get('/api/recipes/:id', (req, res) => {
    const recipe = recipes.find((r) => r.id === Number(req.params.id));
    if (!recipe) {
        res.status(404).json({ error: 'No such recipe' });
        return;
    }
    res.json(recipe);
});

app.post('/api/recipes', (req, res) => {
    const { title, tags } = req.body;
    if (!title) {
        res.status(400).json({ error: 'A recipe needs a title' });
        return;
    }
    const recipe = { id: recipes.length + 1, title, tags: tags ?? [] };
    recipes.push(recipe);
    res.status(201).json(recipe);
});

app.get('/about', (req, res) => {
    res.send('Recipe Box collects family recipes.');
});

app.get('/menu', (req, res) => {
    res.redirect(301, '/recipes');
});

app.get('/menu/:id', (req, res) => {
    res.redirect(301, `/recipes/${req.params.id}`);
});

app.listen(3000, () => {
    console.log('Recipe Box is running on port 3000');
});

All lessons in Express Basics

Practice on your own: Express playground