REST API v Node.js s Express 5: od nuly po validaci a chyby
Postavte REST API v Node.js a Express 5 krok za krokem: struktura projektu, routy, validace se Zod, centrální zpracování chyb a spuštění přes PM2.
Express je pořád nejrozšířenější webový framework pro Node.js. Verze 5 (stabilní od podzimu 2024) konečně umí sama zachytit chyby z async funkcí, takže odpadá většina boilerplatu. Postavíme jednoduché API pro úkoly (todo) s validací a rozumným zpracováním chyb.
Příprava projektu
Potřebujete Node.js 18 nebo novější (Express 5 starší verze nepodporuje).
mkdir todo-api && cd todo-api
npm init -y
npm install express@5 zod helmet
V package.json nastavte ES moduly a skript:
{
"type": "module",
"scripts": {
"dev": "node --watch src/server.js",
"start": "node src/server.js"
}
}
node --watch restartuje server při změně souboru (stabilní od Node.js 22, v 18 a 20 experimentální).
Struktura
src/
app.js # konfigurace Express
server.js # spuštění
routes/todos.js
errors.js
Oddělení app.js a server.js se vyplatí při testování – aplikaci importujete bez otevření portu.
Aplikace
src/app.js:
import express from "express";
import helmet from "helmet";
import todosRouter from "./routes/todos.js";
import { errorHandler, notFound } from "./errors.js";
const app = express();
app.disable("x-powered-by");
app.use(helmet());
app.use(express.json({ limit: "100kb" }));
app.get("/health", (req, res) => res.json({ status: "ok" }));
app.use("/v1/todos", todosRouter);
app.use(notFound);
app.use(errorHandler);
export default app;
src/server.js:
import app from "./app.js";
const port = process.env.PORT || 3000;
app.listen(port, () => console.log(`API běží na http://localhost:${port}`));
Routy s validací
Pro jednoduchost ukládáme data do paměti; v reálné aplikaci sem patří databáze.
src/routes/todos.js:
import { Router } from "express";
import { z } from "zod";
import { HttpError } from "../errors.js";
const router = Router();
const todos = new Map();
let nextId = 1;
const createSchema = z.object({
title: z.string().trim().min(1).max(200),
done: z.boolean().optional().default(false),
});
const updateSchema = createSchema.partial();
function findTodo(id) {
const todo = todos.get(Number(id));
if (!todo) throw new HttpError(404, "Úkol nenalezen");
return todo;
}
router.get("/", (req, res) => {
res.json({ data: [...todos.values()] });
});
router.get("/:id", (req, res) => {
res.json(findTodo(req.params.id));
});
router.post("/", async (req, res) => {
const input = createSchema.parse(req.body);
const todo = { id: nextId++, ...input, createdAt: new Date().toISOString() };
todos.set(todo.id, todo);
res.status(201).location(`/v1/todos/${todo.id}`).json(todo);
});
router.patch("/:id", (req, res) => {
const todo = findTodo(req.params.id);
Object.assign(todo, updateSchema.parse(req.body));
res.json(todo);
});
router.delete("/:id", (req, res) => {
findTodo(req.params.id);
todos.delete(Number(req.params.id));
res.status(204).end();
});
export default router;
schema.parse při neplatných datech vyhodí ZodError. V Express 5 se výjimka ze synchronní i async funkce automaticky předá do error handleru – v Express 4 byste museli každý async handler obalit try/catch a volat next(err).
Centrální zpracování chyb
src/errors.js:
import { ZodError } from "zod";
export class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
export function notFound(req, res) {
res.status(404).json({ error: "Endpoint neexistuje" });
}
// Error handler musí mít čtyři parametry, jinak ho Express nepozná.
export function errorHandler(err, req, res, next) {
if (err instanceof ZodError) {
return res.status(422).json({
error: "Neplatná data",
details: err.issues.map((i) => ({ field: i.path.join("."), message: i.message })),
});
}
if (err.type === "entity.parse.failed") {
return res.status(400).json({ error: "Neplatný JSON" });
}
if (err instanceof HttpError) {
return res.status(err.status).json({ error: err.message });
}
console.error(err);
res.status(500).json({ error: "Interní chyba serveru" });
}
Neočekávané chyby logujeme celé, ale klientovi vracíme jen obecnou zprávu.
Vyzkoušení
npm run dev
curl -i -X POST localhost:3000/v1/todos \
-H "Content-Type: application/json" \
-d '{"title":"Napsat testy"}'
curl -i -X POST localhost:3000/v1/todos \
-H "Content-Type: application/json" \
-d '{"title":""}'
curl -i localhost:3000/v1/todos/999
Očekávané odpovědi: 201 s hlavičkou Location, 422 s detailem pole title a 404.
Rozdíly Express 5 oproti 4, na které narazíte
- Automatické zachytávání odmítnutých promise v handlerech.
- Změněná syntaxe cest: wildcard musí mít jméno (
/*splatmísto/*), volitelné části se píšou/:file{.:ext}. Znak?za parametrem už nefunguje. req.bodyjeundefined, pokud žádný parser nezpracoval tělo (dříve{}).- Odstraněny zastaralé metody jako
res.send(status)– používejteres.sendStatus().
Nasazení s PM2
npm install -g pm2
NODE_ENV=production pm2 start src/server.js --name todo-api
pm2 save
pm2 startup # vypíše příkaz pro automatický start po rebootu
Před aplikaci dejte reverzní proxy (Nginx nebo Caddy) s HTTPS a v Expressu nastavte app.set("trust proxy", 1), aby req.ip odpovídalo skutečnému klientovi.
Co dál
- Databáze – PostgreSQL s knihovnou
pgnebo ORM (Prisma, Drizzle). - Rate limiting – balíček
express-rate-limit, viz rate limiting API. - Testy –
supertestnad exportovanýmappbez spuštění serveru.
Pokud máte projekt nahraný v ColdHost AI (ZIP nebo import z GitHubu), můžete v režimu Programování požádat o přidání perzistence do PostgreSQL – AI upraví soubory a výsledek pushnete jako pull request.