404 처리기
Coddy Node.js 여정의 Express 기초 섹션에 포함된 레슨. 23개 중 16번째.
Express는 알 수 없는 경로에 대해 자체 페이지인 Cannot GET /x를 표시하는데, 이는 사이트에도 API에도 어울리지 않습니다.
app.use((req, res) => {
res.status(404).send('No forecast here. Try /weather/oslo');
});경로가 지정되지 않은 미들웨어는 해당 지점까지 도달하는 모든 요청에서 실행됩니다. 이 미들웨어는 모든 경로와 express.static 뒤에 배치하세요. 경로에서 응답한 요청은 여기에 도달하지 않으므로, 아무도 응답하지 않은 요청만 이 미들웨어에 도달합니다. 이 미들웨어가 응답하므로 next는 필요하지 않습니다.
req.path는 쿼리 문자열을 제외한 경로이므로, req.path.startsWith('/api/')를 사용하면 API 호출과 페이지 요청을 구분할 수 있습니다.
챌린지
쉬움존재하지 않는 경로에는 Express의 기본 Cannot GET 페이지가 표시됩니다. 이를 직접 만든 페이지로 바꾸세요. API에는 JSON을, 방문자에게는 친절한 페이지를 보여 주세요.
server.js에서 모든 경로와express.static뒤에 경로를 지정하지 않은 마지막app.use를 하나 추가하세요. 이미 응답을 받은 요청은 여기에 도달하지 않으므로, 아무것도 처리하지 않은 요청만 이곳에서 확인할 수 있습니다.- 경로가
/api/로 시작하면 상태 코드 404와 다음 JSON을 응답합니다.{"error": "Not found"} - 그 외의 경로에는 상태 코드 404와
Page not found라는 문구가 포함된 HTML 페이지를 응답합니다. 예를 들면<h1>Page not found</h1>와 같습니다. 홈페이지로 돌아가는 링크를 넣으면 좋습니다.
테스트해 보세요: ‘실행’을 누르고 /api/nope를 보내세요. 상태 코드 404와 {"error":"Not found"}가 표시되어야 합니다. /nope를 보내면 상태 코드 404와 함께 Page not found라고 적힌 페이지가 표시됩니다. /api/recipes/1을 보내면 상태 코드 200과 Tomato Soup가 표시됩니다. 이렇게 하면 경로가 여전히 먼저 처리된다는 것을 확인할 수 있습니다.
직접 해보기
import { escapeHtml } from './html.js';
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'));
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')는 일반 텍스트를 전송하므로 각 제목이 한 줄씩 표시됩니다.
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');
});
Express 기초의 모든 레슨
직접 연습해 보세요: Express 플레이그라운드