Menu
CoddyTech

El manejador 404

Parte de la sección Conceptos básicos de Express del Journey de Node.js de Coddy. Lección 14 de 20.

Express responde a una ruta desconocida con su propia página, Cannot GET /x, que no encaja ni con tu sitio ni con tu API.

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

Un middleware sin ruta se ejecuta en cada solicitud que llega hasta él. Coloca este después de todas las rutas y de express.static: las solicitudes que estos atendieron nunca llegan hasta allí, así que solo ve aquellas que nadie atendió. Como responde, no necesita next.

req.path es la ruta sin la cadena de consulta, así que req.path.startsWith('/api/') permite distinguir una llamada a la API de una página.

challenge icon

Desafío

Fácil
  • Una ruta dentro de /api/ a la que no responde ningún controlador devuelve 404 con { "error": "Not found" }.
  • Cualquier otra ruta devuelve una página HTML 404 que dice Page not found.

Envía GET /api/nope, abre /nope en la vista de página y después envía GET /api/recipes/1.

Pruébalo tú mismo

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'));

const escapeHtml = (text) =>
    text
        .replaceAll('&', '&')
        .replaceAll('<', '&lt;')
        .replaceAll('>', '&gt;')
        .replaceAll('"', '&quot;');

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') envía texto sin formato, por lo que cada título aparece en su propia línea.
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');
});

Todas las lecciones de Conceptos básicos de Express