Node.js — JS na serveru
Node.js je runtime pro JavaScript mimo prohlížeč — postavený na V8 enginu Chrome. Single-threaded event loop s non-blocking I/O umožňuje obsloužit tisíce souběžných requestů bez tradičního thread-per-request modelu.
| Vlastnost | Node.js | Tradiční server (PHP, Java) |
|---|---|---|
| Vlákna | Single thread + event loop | Thread per request |
| I/O | Non-blocking (async) | Blocking (synchronní) |
| Škálovatelnost | Výborná pro I/O operace | Dobrá pro CPU operace |
| Jazyk | JavaScript (nebo TypeScript) | PHP, Java, C#, Python... |
| Ekosystém | npm — 2M+ balíčků | Záleží na jazyku |
| Real-time | Výborný (WebSockets) | Složitější |
Express — základní setup
// src/app.ts — hlavní Express aplikace
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import { productsRouter } from './routes/products';
import { usersRouter } from './routes/users';
import { errorHandler } from './middleware/errorHandler';
const app = express();
// ── Middleware ──────────────────────────────────────────
app.use(helmet()); // Bezpečnostní HTTP hlavičky
app.use(cors({ // CORS — povolí frontend origin
origin: process.env.FRONTEND_URL ?? 'http://localhost:3000',
credentials: true,
}));
app.use(morgan('dev')); // Request logging
app.use(express.json()); // Parsovat JSON tělo requestu
app.use(express.urlencoded({ extended: true }));
// ── Routes ─────────────────────────────────────────────
app.use('/api/products', productsRouter);
app.use('/api/users', usersRouter);
// Health check
app.get('/health', (req, res) => res.json({ status: 'ok' }));
// ── Error handling (musí být poslední!) ────────────────
app.use(errorHandler);
export default app;
// src/server.ts — start serveru
import app from './app';
const PORT = process.env.PORT ?? 4000;
app.listen(PORT, () => {
console.log(`🚀 Server běží na http://localhost:${PORT}`);
});
package.json scripts
{
"scripts": {
"dev": "nodemon --exec ts-node src/server.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js",
"lint": "eslint src/"
}
}
Routing & Controllers
Profesionální Express API odděluje routing (URL + HTTP metoda) od business logiky (controller). Controller volá service vrstvu, která komunikuje s databází.
// src/routes/products.ts — Router
import { Router } from 'express';
import { authenticate } from '../middleware/auth';
import * as ctrl from '../controllers/productsController';
import { validate } from '../middleware/validate';
import { createProductSchema } from '../schemas/product';
export const productsRouter = Router();
// Veřejné routes
productsRouter.get('/', ctrl.getProducts);
productsRouter.get('/:id', ctrl.getProductById);
// Chráněné routes (vyžadují JWT)
productsRouter.post('/',
authenticate,
validate(createProductSchema),
ctrl.createProduct
);
productsRouter.put('/:id', authenticate, ctrl.updateProduct);
productsRouter.delete('/:id', authenticate, ctrl.deleteProduct);
// src/controllers/productsController.ts
import { Request, Response, NextFunction } from 'express';
import { ProductsService } from '../services/productsService';
const service = new ProductsService();
export async function getProducts(
req: Request,
res: Response,
next: NextFunction
) {
try {
const { page = '1', limit = '10', search } = req.query;
const result = await service.findAll({
page: parseInt(page as string),
limit: parseInt(limit as string),
search: search as string,
});
res.json(result);
} catch (error) {
next(error); // Předá do error middleware!
}
}
export async function createProduct(
req: Request,
res: Response,
next: NextFunction
) {
try {
const product = await service.create(req.body);
res.status(201).json(product);
} catch (error) {
next(error);
}
}
Middleware
Middleware je funkce (req, res, next) => void která zpracovává request před nebo po route handleru. Tvoří řetězec — každý middleware buď odpoví, nebo zavolá next().
// src/middleware/validate.ts — Zod validace
import { AnyZodObject, ZodError } from 'zod';
import { Request, Response, NextFunction } from 'express';
export function validate(schema: AnyZodObject) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
// Validuje body, query a params najednou
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (error) {
if (error instanceof ZodError) {
return res.status(400).json({
error: 'Validační chyba',
details: error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
})),
});
}
next(error);
}
};
}
// src/schemas/product.ts — Zod schema
import { z } from 'zod';
export const createProductSchema = z.object({
body: z.object({
nazev: z.string().min(2).max(100),
cena: z.number().positive().max(999999),
popis: z.string().optional(),
dostupny: z.boolean().default(true),
}),
});
// src/middleware/rateLimiter.ts — rate limiting
import rateLimit from 'express-rate-limit';
export const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minut
max: 100, // max 100 requestů per IP
message: { error: 'Příliš mnoho requestů, zkuste znovu za 15 minut' },
});
// Kvíz: Co se stane, když middleware v Express nezavolá next() ani nepošle response?
Autentizace & JWT
JWT (JSON Web Token) je bezstavový autentizační mechanismus — server nevede session, všechny potřebné informace jsou v tokenu samotném. Tři části: header, payload, signature.
// npm install jsonwebtoken bcryptjs
// npm install -D @types/jsonwebtoken @types/bcryptjs
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import { Request, Response, NextFunction } from 'express';
const JWT_SECRET = process.env.JWT_SECRET!; // NIKDY hardcode!
const JWT_EXPIRY = '7d';
// ── Registrace ───────────────────────────────────────────
export async function register(req: Request, res: Response) {
const { email, password, name } = req.body;
const exists = await db.user.findUnique({ where: { email } });
if (exists) return res.status(409).json({ error: 'Email již existuje' });
// Hashování hesla — NIKDY neukládat plaintext!
const hashedPassword = await bcrypt.hash(password, 12); // 12 = cost factor
const user = await db.user.create({
data: { email, name, password: hashedPassword },
select: { id: true, email: true, name: true } // NEvracíme heslo!
});
const token = jwt.sign({ userId: user.id, email }, JWT_SECRET, {
expiresIn: JWT_EXPIRY
});
res.status(201).json({ user, token });
}
// ── Přihlášení ───────────────────────────────────────────
export async function login(req: Request, res: Response) {
const { email, password } = req.body;
const user = await db.user.findUnique({ where: { email } });
// Konstantní čas pro bezpečnost (zabrání timing attack)
const isValid = user
? await bcrypt.compare(password, user.password)
: await bcrypt.compare(password, '$2b$12$placeholder'); // dummy hash
if (!user || !isValid) {
return res.status(401).json({ error: 'Neplatné přihlašovací údaje' });
}
const token = jwt.sign({ userId: user.id }, JWT_SECRET, {
expiresIn: JWT_EXPIRY
});
res.json({ token, user: { id: user.id, email, name: user.name } });
}
// ── Authenticate middleware ──────────────────────────────
interface AuthRequest extends Request {
user?: { userId: number; email: string };
}
export function authenticate(
req: AuthRequest,
res: Response,
next: NextFunction
) {
const authHeader = req.headers.authorization;
const token = authHeader?.startsWith('Bearer ')
? authHeader.slice(7)
: null;
if (!token) {
return res.status(401).json({ error: 'Chybí autentizační token' });
}
try {
const decoded = jwt.verify(token, JWT_SECRET) as { userId: number; email: string };
req.user = decoded; // Přidáme na request
next();
} catch (error) {
return res.status(401).json({ error: 'Neplatný nebo expirovaný token' });
}
}
// Kvíz: Proč je důležité používat bcrypt.compare() se stejnou dobou i pro neexistujícího uživatele?
Error Handling
Centralizovaný error handler zpracovává všechny chyby jednotně. Express error middleware má 4 parametry: (err, req, res, next).
// src/errors/AppError.ts — vlastní chybové třídy
export class AppError extends Error {
constructor(
public message: string,
public statusCode: number = 500,
public code?: string,
) {
super(message);
this.name = 'AppError';
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource = 'Zdroj') {
super(`${resource} nebyl nalezen`, 404, 'NOT_FOUND');
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Neautorizovaný přístup') {
super(message, 401, 'UNAUTHORIZED');
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(message, 400, 'VALIDATION_ERROR');
}
}
// src/middleware/errorHandler.ts — centrální handler
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/AppError';
import { ZodError } from 'zod';
import { Prisma } from '@prisma/client';
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
// Naše vlastní chyby
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: err.message,
code: err.code,
});
}
// Zod validační chyby
if (err instanceof ZodError) {
return res.status(400).json({
error: 'Validační chyba',
details: err.errors,
});
}
// Prisma chyby (databáze)
if (err instanceof Prisma.PrismaClientKnownRequestError) {
if (err.code === 'P2002') { // unique constraint
return res.status(409).json({ error: 'Záznam již existuje' });
}
if (err.code === 'P2025') { // record not found
return res.status(404).json({ error: 'Záznam nenalezen' });
}
}
// Neočekávané chyby — logovat, ale nevracet detaily klientovi
console.error('Unhandled error:', err);
res.status(500).json({ error: 'Interní chyba serveru' });
}
// Použití v controlleru:
export async function getProduct(req, res, next) {
try {
const product = await service.findById(Number(req.params.id));
if (!product) throw new NotFoundError('Produkt');
res.json(product);
} catch (error) {
next(error); // ← vždy předat do error middleware!
}
}
Struktura produkčního API projektu
// Kvíz: Proč je důležité v error handleru nevracet detailní stack trace klientovi v produkci?
Cvičení — REST API pro blog
Navrhněte kompletní Express REST API pro blog systém. Definujte routes, middleware stack, schema validace a error scénáře.
- Auth: POST /auth/register, POST /auth/login
- Posts: CRUD + GET /posts?page=1&search=nodejs
- Comments: GET/POST /posts/:id/comments
- Middleware: authenticate, validate (Zod), rateLimiter
- Error handling: NotFoundError pro neexistující post
- Co musí být chráněno JWT a co je veřejné?
// Navrhněte router:
const postsRouter = Router();
// Veřejné
postsRouter.get('/', /* co sem patří? */);
postsRouter.get('/:id', /* validate params? */);
// Chráněné
postsRouter.post('/',
/* middleware stack */
);
postsRouter.put('/:id',
/* vlastník nebo admin? */
);
// src/schemas/post.ts
export const createPostSchema = z.object({
body: z.object({
// Navrhněte fieldy pro blog post
title: /* validace délky */,
content: /* min délka */,
tags: /* pole stringů */,
published: /* boolean, default false */,
}),
});
// src/services/postsService.ts
export class PostsService {
async findAll({ page, limit, search }: FindAllParams) {
// Navrhněte logiku: skip, take, where clause pro search
}
}
Node.js API Engineer
Zvládáte Express, middleware, JWT auth, Zod validaci a error handling.
// Taháček
// Express boilerplate
const app = express();
app.use(helmet()); app.use(cors()); app.use(express.json());
app.use('/api/resource', resourceRouter);
app.use(errorHandler); // ← vždy poslední!
// Middleware signature
(req: Request, res: Response, next: NextFunction) => void
// JWT
const token = jwt.sign({ userId }, SECRET, { expiresIn: '7d' });
const decoded = jwt.verify(token, SECRET) as JwtPayload;
// bcrypt
const hash = await bcrypt.hash(password, 12);
const valid = await bcrypt.compare(plaintext, hash);
// Error: always next(error) — nikdy throw bez catch
try {
const data = await service.findById(id);
if (!data) throw new NotFoundError('Resource');
res.json(data);
} catch (err) { next(err); }
// Zod validate middleware
app.post('/resource', validate(schema), controller);