Menu
CoddyTech

Suchseite

Teil des Abschnitts Express-Grundlagen der Node.js-Journey von Coddy. Lektion 20 von 20.

Eine Suchseite ist ein Formular, und der Server beantwortet es wie jede andere GET-Anfrage.

<form action="/books" method="get">
    <input name="author">
    <button>Find</button>
</form>

Beim Absenden dieses Formulars wird der Browser zu /books?author= weitergeleitet, gefolgt von dem, was in das Feld mit dem Namen author eingegeben wurde. Die Route liest den Wert aus req.query. Wenn du das Wort als value des Feldes zurückschreibst, bleibt es nach der Suche sichtbar; es stammt vom Besucher, also musst du es maskieren.

challenge icon

Aufgabe

Einfach
  • GET /search?q=soup liefert eine Seite mit einem GET-Formular (Feld q, gesendet an /search) und Links zu den Titeln aus loadRecipes(), die das Wort unabhängig von der Groß- und Kleinschreibung enthalten.
  • Kein Treffer: No recipes found.
  • Das Feld behält das Wort bei, nachdem es escaped wurde.

Öffne /search in der Seitenansicht und suche nach SOUP, dann nach pizza.

Probier es selbst

import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';
import recipesRouter, { loadRecipes } from './routes/recipes.js';
import reviewsRouter from './routes/reviews.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('&', '&amp;')
        .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') sendet reinen Text, sodass jeder Titel in einer eigenen Zeile steht.
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.use('/api/recipes', recipesRouter);
app.use('/api/recipes/:id/reviews', reviewsRouter);

app.get('/api/today', async (req, res) => {
    const date = req.query.date ? new Date(req.query.date) : new Date();
    if (Number.isNaN(date.getTime())) {
        res.status(400).json({ error: 'Use a date like 2026-03-01' });
        return;
    }
    const day = Math.floor((date - Date.UTC(date.getUTCFullYear(), 0, 0)) / 86400000);
    const recipes = await loadRecipes();
    res.json(recipes[day % recipes.length]);
});

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.use((req, res) => {
    if (req.path.startsWith('/api/')) {
        res.status(404).json({ error: 'Not found' });
        return;
    }
    res.status(404).send('<h1>Page not found</h1><p><a href="/">Back to Recipe Box</a></p>');
});

app.use((err, req, res, next) => {
    console.error(err);
    res.status(500).json({ error: 'Something went wrong' });
});

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

Alle Lektionen in Express-Grundlagen