Menu
CoddyTech

Page de recherche

Fait partie de la section Les bases d’Express du Journey Node.js de Coddy. Leçon 23 sur 23.

Une page de recherche est un formulaire, et le serveur y répond comme à toute autre requête GET.

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

La soumission de ce formulaire redirige le navigateur vers /books?author=, suivi du texte saisi dans le champ nommé author. La route le récupère dans req.query. Réécrire le mot dans la propriété value du champ le laisse visible après la recherche ; il provient du visiteur, alors échappe-le.

challenge icon

Défi

Facile

Recipe Box se dote d’une page de recherche. Un visiteur saisit un mot, et la page affiche les recettes dont le titre le contient.

  1. Dans server.js, au-dessus du gestionnaire 404, ajoute une route asynchrone pour /search. Elle lit le mot dans req.query.q, ou utilise une chaîne vide s’il n’y en a pas.
  2. La page affiche toujours un formulaire de recherche : <form action="/search" method="get"> avec un champ de texte nommé q et un bouton. En le soumettant, on ouvre /search?q= suivi du mot.
  3. Le champ affiche le mot recherché : après avoir recherché curry, le champ contient curry. Écris le mot dans l’attribut value du champ, en le faisant passer par escapeHtml, car il provient du visiteur.
  4. Sous le formulaire, affiche un lien pour chaque recette de loadRecipes() dont le titre contient le mot, sans tenir compte des majuscules : soup et SOUP trouvent toutes deux Tomato Soup. Chaque lien mène à la page de la recette et affiche son titre échappé, comme sur la page d’accueil : <a href="/recipes/1">Tomato Soup</a>.
  5. Lorsqu’un mot a été recherché et qu’aucun titre ne le contient, affiche le texte No recipes found à la place de la liste.

Essaie : Appuie sur Run et envoie /search : la page affiche le formulaire vide. Saisis SOUP dans le champ et appuie sur le bouton. La page affiche Tomato Soup, et le champ contient toujours SOUP. Recherche pizza : No recipes found. Recherche fish : Fish & Chips s’affiche correctement. Ensuite, envoie {"title": "Pea Soup", "tags": ["soup"]} à /api/recipes, puis recherche à nouveau soup : Pea Soup figure aussi dans la liste.

Essayez vous-même

import { escapeHtml } from './html.js';
import { dayOfYear } from './day-of-year.js';
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'));

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') envoie du texte brut, donc chaque titre s’affiche sur sa propre ligne.
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 day = dayOfYear(req.query.date);
    if (day === null) {
        res.status(400).json({ error: 'Use a date like 2026-03-01' });
        return;
    }
    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');
});

Toutes les leçons de Les bases d’Express

Entraînez-vous par vous-même : Playground Express