Programování

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.

12. 10. 2026 · 5 min čtení

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 (/*splat místo /*), volitelné části se píšou /:file{.:ext}. Znak ? za parametrem už nefunguje.
  • req.body je undefined, pokud žádný parser nezpracoval tělo (dříve {}).
  • Odstraněny zastaralé metody jako res.send(status) – používejte res.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 pg nebo ORM (Prisma, Drizzle).
  • Rate limiting – balíček express-rate-limit, viz rate limiting API.
  • Testy – supertest nad exportovaným app bez 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.