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.

VlastnostNode.jsTradiční server (PHP, Java)
VláknaSingle thread + event loopThread per request
I/ONon-blocking (async)Blocking (synchronní)
ŠkálovatelnostVýborná pro I/O operaceDobrá pro CPU operace
JazykJavaScript (nebo TypeScript)PHP, Java, C#, Python...
Ekosystémnpm — 2M+ balíčkůZáleží na jazyku
Real-timeVýborný (WebSockets)Složitější
bash — Node.js setup
# Doporučeno: nvm pro správu verzí $ nvm install --lts $ node --version v20.11.0 # Nový Express projekt s TypeScriptem $ mkdir moje-api && cd moje-api $ npm init -y $ npm install express cors helmet morgan zod $ npm install -D typescript @types/express @types/node ts-node nodemon $ npx tsc --init
🔄
Express vs alternativy v 2024: Express je stále #1 (nejznámější, nejvíce zdrojů). Hono je moderní rychlá alternativa s TypeScript-first API. Fastify je výkonnostně lepší pro high-throughput. Elysia (Bun runtime) — nejnovější.
// 01 / 09 Express setup →

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/"
  }
}
// 02 / 09 Routing →

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);
  }
}
// 03 / 09 Middleware →

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().

Request
HTTP request
helmet()
Security headers
cors()
CORS check
authenticate
JWT verify
validate
Zod schema
Controller
Business logic
Response
JSON back
// 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?

// 04 / 09 Auth & JWT →

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' });
  }
}
🔐
JWT bezpečnost: JWT_SECRET musí být min. 256-bit random string — nikdy pevně v kódu, vždy z env vars. Refresh tokeny ukládat v httpOnly cookie (ne localStorage). Access token expirace: 15min–1h. Refresh token: 7–30 dní.

// Kvíz: Proč je důležité používat bcrypt.compare() se stejnou dobou i pro neexistujícího uživatele?

// 05 / 09 Error handling →

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!
  }
}
// 06 / 09 Struktura →

Struktura produkčního API projektu

src/
routes/ ← URL + HTTP metoda
products.ts
users.ts
auth.ts
controllers/ ← Req/res handling
productsController.ts
services/ ← Business logika
productsService.ts
middleware/ ← Cross-cutting concerns
auth.ts ← JWT authenticate
validate.ts ← Zod validace
errorHandler.ts ← Centrální error
rateLimiter.ts
schemas/ ← Zod schemas
errors/ ← AppError třídy
lib/ ← DB client, utils
prisma.ts ← Prisma singleton
app.ts ← Express app
server.ts ← Start server
.env ← Secrets (NIKDY do Gitu!)
.env.example
tsconfig.json

// Kvíz: Proč je důležité v error handleru nevracet detailní stack trace klientovi v produkci?

// 07 / 09 Cvičení →

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.

📋
Požadavky:
  • 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.

// 08 / 09 Taháček →

// 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);
// Lekce 5 dokončena L6: Databáze & Prisma →