Menu
CoddyTech

Halaman Pencarian

Termasuk bagian Dasar-Dasar Express dalam perjalanan Node.js di Coddy. Pelajaran 20 dari 20.

Halaman pencarian adalah formulir, dan server menanganinya seperti permintaan GET lainnya.

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

Mengirim formulir ini akan membawa browser ke /books?author= diikuti oleh teks yang diketik di bidang bernama author. Rute membaca nilainya dari req.query. Menuliskan kembali kata tersebut sebagai value bidang akan membuatnya tetap terlihat setelah pencarian. Kata itu berasal dari pengunjung, jadi lakukan escape.

challenge icon

Tantangan

Mudah
  • GET /search?q=soup menampilkan halaman dengan formulir GET (kolom q, dikirim ke /search) dan tautan ke judul yang memuat kata tersebut, tanpa membedakan huruf besar-kecil, dari loadRecipes().
  • Tidak ada yang cocok: No recipes found.
  • Kolom tersebut mempertahankan kata yang dimasukkan, dengan karakter yang di-escape.

Buka /search pada tampilan halaman dan cari SOUP, lalu pizza.

Coba sendiri

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') mengirim teks biasa, sehingga setiap judul tampil di baris tersendiri.
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');
});

Semua pelajaran di Dasar-Dasar Express

Latihan sendiri: Playground Express