Menu
CoddyTech

Công thức hôm nay

Thuộc phần Kiến thức cơ bản về Express trong hành trình Node.js của Coddy. Bài học 22 trên 23.

Recipe Box có thể dùng các kỹ năng quen thuộc về định tuyến và đọc tệp để cung cấp một công thức cho mỗi ngày. Hàm hỗ trợ dayOfYear được cung cấp sẽ chuyển ngày thành một con số: ngày 1 tháng 1 là 1 và ngày 1 tháng 3 năm 2026 là 60. Nếu thiếu ngày, hệ thống sẽ dùng ngày hôm nay; văn bản không đọc được sẽ trả về null.

const day = dayOfYear(req.query.date);
// ?date=2026-03-01 cho ra 60
// ?date=soon cho ra null

Dùng toán tử lấy phần dư để chọn một vị trí: day % recipes.length. Với bốn công thức, ngày 60 chọn vị trí không. Hãy tải các công thức bên trong yêu cầu để cả những công thức mới đăng cũng được tính đến. Phần phân tích ngày đã có sẵn; hãy tập trung vào việc xây dựng tuyến.

challenge icon

Thử thách

Dễ

Thêm endpoint Món ăn hôm nay vào Recipe Box. Tệp day-of-year.js được cung cấp xuất dayOfYear(text), hàm này trả về số thứ tự ngày hoặc null nếu ngày không hợp lệ.

  1. Xuất loadRecipes từ routes/recipes.js. Nhập hàm này vào server.js: router vẫn là import mặc định, còn { loadRecipes } theo sau router, cách nhau bằng dấu phẩy.
  2. Nhập dayOfYear. Trong server.js, thêm một route async cho GET /api/today phía trên trình xử lý 404. Truyền req.query.date cho hàm hỗ trợ.
  3. Nếu kết quả là null, trả về mã 400 cùng với {"error":"Use a date like 2026-03-01"}.
  4. Nếu không, hãy chờ loadRecipes() hoàn tất rồi trả về recipes[day % recipes.length] dưới dạng JSON. Giữ nguyên các route hiện có và trình xử lý lỗi.

Thử GET /api/today?date=2026-03-01: endpoint trả về món ăn đầu tiên vì số thứ tự ngày là 60 và có bốn món ăn. Thử ?date=soon: endpoint trả về lỗi 400 ở trên.

Tự làm thử

import { escapeHtml } from './html.js';
import express from 'express';
import { randomUUID } from 'node:crypto';
import { recipes } from './data.js';
import recipesRouter 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') gửi văn bản thuần túy, nên mỗi tiêu đề hiển thị trên một dòng riêng.
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('/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');
});

Tất cả bài học trong Kiến thức cơ bản về Express

Tự luyện tập: Playground Express