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
EasyPaths 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.
- In
server.js, after every route and afterexpress.static, add one lastapp.usewith no path. Requests that something already responded to never reach it, so it sees only the ones nothing handled. - When the path starts with
/api/, it responds with status 404 and this JSON:{"error": "Not found"} - 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
3Middleware
Your First MiddlewarePractice on your own: Express playground