Node.js + TypeScript · 30 Lessons · Windows

Backend Mastery
від нуля до профі

A complete journey from zero to backend developer — with real service programs, tests, and TypeScript on every step.

Node.js TypeScript Jest Express REST API PostgreSQL Docker Windows
01
Node.js & TypeScript Setup on Windows
TypeScript Node.js Jest
🇺🇦 Українська
У цьому уроці ми налаштуємо повноцінне середовище для backend-розробки на Windows. Node.js — це середовище виконання JavaScript поза браузером. TypeScript додає статичну типізацію, що робить код надійнішим. Ми встановимо всі необхідні інструменти і створимо перший TypeScript-проєкт з правильною структурою.
🇬🇧 English
In this lesson we set up a full backend development environment on Windows. Node.js is a JavaScript runtime outside the browser. TypeScript adds static typing that makes code more reliable. We'll install all necessary tools and create the first TypeScript project with a proper structure.
Installation Steps
  • 1Download Node.js LTS from nodejs.org — choose the Windows Installer (.msi). After install, run: node -v
  • 2Install TypeScript globally: npm install -g typescript ts-node nodemon
  • 3Create project folder and init npm + TypeScript config
  • 4Install dev dependencies for testing and building
Project Initialization
Windows PowerShell / CMD
PS> mkdir my-backend && cd my-backend
PS> npm init -y
PS> npm install -D typescript @types/node ts-node nodemon jest @types/jest ts-jest
PS> npx tsc --init
tsconfig.json
tsconfig.json JSON
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}
package.json Scripts
package.json (scripts section) JSON
"scripts": {
  "dev": "nodemon --exec ts-node src/index.ts",
  "build": "tsc",
  "start": "node dist/index.js",
  "test": "jest --coverage",
  "test:watch": "jest --watch"
}
First TypeScript File
src/index.ts TypeScript
// Entry point of the backend application
const greet = (name: string): string => {
  return `Hello, ${name}! Backend is running.`;
};

console.log(greet('World'));
console.log(`Node.js version: ${process.version}`);
Test File
✓ Jest Test — src/index.test.ts
import { greet } from './utils/greet';

describe('greet()', () => {
  it('should return greeting string', () => {
    const result = greet('Alice');
    expect(result).toBe('Hello, Alice! Backend is running.');
  });

  it('should handle empty string', () => {
    const result = greet('');
    expect(result).toContain('Hello');
  });
});
npm run test
PASS  src/index.test.ts
  greet()
    ✓ should return greeting string (3ms)
    ✓ should handle empty string (1ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
02
TypeScript Fundamentals for Backend
TypeScript Testing
🇺🇦 Українська
TypeScript — надмножина JavaScript із системою типів. Для backend-розробки це критично важливо: інтерфейси описують форму даних (наприклад, тіло запиту), union-типи дозволяють моделювати різні стани, generics роблять функції гнучкими. У цьому уроці ми вивчимо всі ключові конструкції TypeScript, які використовуються у backend щодня.
🇬🇧 English
TypeScript is a superset of JavaScript with a type system. For backend development this is critical: interfaces describe data shapes (e.g. request bodies), union types model different states, generics make functions flexible. In this lesson we learn all key TypeScript constructs used in backend daily.
Types & Interfaces
src/types/user.tsTypeScript
// Interfaces — define the shape of objects
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator'; // union type
  createdAt: Date;
  bio?: string; // optional field
}

// Type alias — good for unions and computed types
type CreateUserDTO = Omit<User, 'id' | 'createdAt'>;
type UpdateUserDTO = Partial<CreateUserDTO>;

// Generic function
function findById<T extends { id: number }>(
  items: T[],
  id: number
): T | undefined {
  return items.find((item) => item.id === id);
}

// Result type — for safe error handling
type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

export { User, CreateUserDTO, UpdateUserDTO, Result, findById };
Enums & Type Guards
src/types/enums.tsTypeScript
enum HttpStatus {
  OK = 200,
  CREATED = 201,
  BAD_REQUEST = 400,
  UNAUTHORIZED = 401,
  NOT_FOUND = 404,
  INTERNAL_ERROR = 500,
}

// Type guard — narrow union types at runtime
function isUser(value: unknown): value is User {
  return (
    typeof value === 'object' &&
    value !== null &&
    'id' in value &&
    'email' in value
  );
}

export { HttpStatus, isUser };
Tests for TypeScript Utilities
✓ Jest Test — src/types/user.test.ts
import { findById, isUser } from './user';

describe('findById', () => {
  const users = [
    { id: 1, name: 'Alice' },
    { id: 2, name: 'Bob' },
  ];

  it('finds existing item', () => {
    expect(findById(users, 1)).toEqual({ id: 1, name: 'Alice' });
  });

  it('returns undefined for missing item', () => {
    expect(findById(users, 99)).toBeUndefined();
  });
});

describe('isUser type guard', () => {
  it('returns true for valid user shape', () => {
    expect(isUser({ id: 1, email: 'a@b.com' })).toBe(true);
  });
  it('returns false for non-object', () => {
    expect(isUser('string')).toBe(false);
  });
});
03
Modules & File System Service
Node.js TypeScript Jest
🇺🇦 Українська
Node.js надає вбудовані модулі для роботи з файловою системою (fs), шляхами (path) та операційною системою (os). Ми побудуємо реальний сервіс для читання/запису файлів з типізацією. Це базова навичка backend-розробника — зберігання даних у файлах, читання конфігурацій, логування.
🇬🇧 English
Node.js provides built-in modules for the file system (fs), paths (path), and operating system (os). We'll build a real typed file service. This is a fundamental backend skill — storing data in files, reading configs, logging.
File Service Implementation
src/services/FileService.tsTypeScript
import * as fs from 'fs/promises';
import * as path from 'path';

interface FileServiceOptions {
  encoding: BufferEncoding;
  basePath: string;
}

class FileService {
  private basePath: string;
  private encoding: BufferEncoding;

  constructor(options: Partial<FileServiceOptions> = {}) {
    this.basePath = options.basePath ?? process.cwd();
    this.encoding = options.encoding ?? 'utf-8';
  }

  private resolvePath(filename: string): string {
    return path.resolve(this.basePath, filename);
  }

  async readFile(filename: string): Promise<string> {
    const filePath = this.resolvePath(filename);
    return fs.readFile(filePath, { encoding: this.encoding });
  }

  async writeFile(filename: string, content: string): Promise<void> {
    const filePath = this.resolvePath(filename);
    await fs.mkdir(path.dirname(filePath), { recursive: true });
    await fs.writeFile(filePath, content, this.encoding);
  }

  async readJson<T>(filename: string): Promise<T> {
    const raw = await this.readFile(filename);
    return JSON.parse(raw) as T;
  }

  async writeJson<T>(filename: string, data: T): Promise<void> {
    await this.writeFile(filename, JSON.stringify(data, null, 2));
  }

  async exists(filename: string): Promise<boolean> {
    try {
      await fs.access(this.resolvePath(filename));
      return true;
    } catch {
      return false;
    }
  }
}

export { FileService };
✓ Jest Test — src/services/FileService.test.ts
import { FileService } from './FileService';
import * as os from 'os';
import * as path from 'path';

describe('FileService', () => {
  const tmpDir = path.join(os.tmpdir(), 'file-service-test');
  const service = new FileService({ basePath: tmpDir });

  it('writes and reads a text file', async () => {
    await service.writeFile('hello.txt', 'Hello World');
    const content = await service.readFile('hello.txt');
    expect(content).toBe('Hello World');
  });

  it('writes and reads JSON', async () => {
    const data = { name: 'test', value: 42 };
    await service.writeJson('data.json', data);
    const read = await service.readJson<{ name: string; value: number }>('data.json');
    expect(read).toEqual(data);
  });

  it('returns true for existing file', async () => {
    await service.writeFile('exists.txt', 'yes');
    expect(await service.exists('exists.txt')).toBe(true);
  });
});
04
First HTTP Server (Pure Node.js)
Node.js http TypeScript
🇺🇦 Українська
Перш ніж використовувати фреймворки, важливо зрозуміти, як Node.js обробляє HTTP-запити на низькому рівні. Вбудований модуль http дозволяє створити сервер без жодних залежностей. Ми побудуємо простий JSON-API сервер, що демонструє основи: маршрутизація, методи HTTP, формат відповіді.
🇬🇧 English
Before using frameworks, it's important to understand how Node.js handles HTTP requests at a low level. The built-in http module lets you create a server with zero dependencies. We'll build a simple JSON API server demonstrating the basics: routing, HTTP methods, response format.
src/server/httpServer.tsTypeScript
import http from 'http';

interface ApiResponse<T = unknown> {
  success: boolean;
  data?: T;
  error?: string;
}

const sendJson = <T>(
  res: http.ServerResponse,
  status: number,
  body: ApiResponse<T>
) => {
  res.writeHead(status, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(body));
};

const server = http.createServer(async (req, res) => {
  const { method, url } = req;

  if (url === '/health' && method === 'GET') {
    return sendJson(res, 200, { success: true, data: { status: 'ok' } });
  }

  if (url === '/api/echo' && method === 'POST') {
    let body = '';
    req.on('data', (chunk) => (body += chunk));
    req.on('end', () => {
      try {
        const parsed = JSON.parse(body);
        sendJson(res, 200, { success: true, data: parsed });
      } catch {
        sendJson(res, 400, { success: false, error: 'Invalid JSON' });
      }
    });
    return;
  }

  sendJson(res, 404, { success: false, error: 'Not found' });
});

export const PORT = 3000;
server.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`);
});

export { server };
Test with curl / PowerShell
PS> curl http://localhost:3000/health
{"success":true,"data":{"status":"ok"}}
PS> curl -X POST http://localhost:3000/api/echo -H "Content-Type: application/json" -d '{"msg":"hi"}'
{"success":true,"data":{"msg":"hi"}}
05
Express.js Framework with TypeScript
Express TypeScript Supertest
🇺🇦 Українська
Express.js — найпопулярніший веб-фреймворк для Node.js. Він надає зручну систему маршрутизації, middleware, обробку помилок. Разом з TypeScript-типами (@types/express) ми отримуємо повну підтримку IntelliSense і безпеку типів для request/response об'єктів.
🇬🇧 English
Express.js is the most popular web framework for Node.js. It provides a convenient routing system, middleware, error handling. Combined with TypeScript types (@types/express) we get full IntelliSense support and type safety for request/response objects.
Install
PS> npm install express
PS> npm install -D @types/express supertest @types/supertest
src/app.tsTypeScript
import express, { Application, Request, Response, NextFunction } from 'express';

const app: Application = express();

// Built-in middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Routes
app.get('/health', (_req: Request, res: Response) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.post('/api/echo', (req: Request, res: Response) => {
  res.status(200).json({ echo: req.body });
});

// 404 handler
app.use((_req: Request, res: Response) => {
  res.status(404).json({ error: 'Route not found' });
});

// Global error handler
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
  res.status(500).json({ error: err.message });
});

export { app };
✓ Supertest Integration Test — src/app.test.ts
import request from 'supertest';
import { app } from './app';

describe('Express App', () => {
  describe('GET /health', () => {
    it('returns 200 with status ok', async () => {
      const res = await request(app).get('/health');
      expect(res.status).toBe(200);
      expect(res.body.status).toBe('ok');
    });
  });

  describe('POST /api/echo', () => {
    it('echoes the request body', async () => {
      const payload = { message: 'hello' };
      const res = await request(app)
        .post('/api/echo')
        .send(payload)
        .set('Content-Type', 'application/json');
      expect(res.status).toBe(200);
      expect(res.body.echo).toEqual(payload);
    });
  });

  describe('Unknown routes', () => {
    it('returns 404', async () => {
      const res = await request(app).get('/nonexistent');
      expect(res.status).toBe(404);
    });
  });
});
06
REST API Design — CRUD Service
REST API TypeScript Testing
🇺🇦 Українська
REST (Representational State Transfer) — архітектурний стиль для API. CRUD означає: Create (POST), Read (GET), Update (PUT/PATCH), Delete (DELETE). Ми побудуємо повноцінний CRUD-сервіс для управління задачами (Task Manager) із правильними HTTP-статусами, маршрутами та router-структурою.
🇬🇧 English
REST (Representational State Transfer) is an architectural style for APIs. CRUD means: Create (POST), Read (GET), Update (PUT/PATCH), Delete (DELETE). We'll build a full CRUD service for task management with proper HTTP status codes, routes, and router structure.
src/routes/tasks.router.tsTypeScript
import { Router, Request, Response } from 'express';

interface Task {
  id: string;
  title: string;
  done: boolean;
  createdAt: string;
}

// In-memory storage (replaced by DB later)
const tasks: Map<string, Task> = new Map();

const generateId = (): string =>
  Math.random().toString(36).slice(2);

const router = Router();

// GET /tasks — list all
router.get('/', (_req: Request, res: Response) => {
  res.json([...tasks.values()]);
});

// GET /tasks/:id — get one
router.get('/:id', (req: Request, res: Response) => {
  const task = tasks.get(req.params.id);
  if (!task) return res.status(404).json({ error: 'Task not found' });
  res.json(task);
});

// POST /tasks — create
router.post('/', (req: Request<{}, {}, { title: string }>, res: Response) => {
  if (!req.body.title) {
    return res.status(400).json({ error: 'Title is required' });
  }
  const task: Task = {
    id: generateId(),
    title: req.body.title,
    done: false,
    createdAt: new Date().toISOString(),
  };
  tasks.set(task.id, task);
  res.status(201).json(task);
});

// PATCH /tasks/:id — update
router.patch('/:id', (req: Request, res: Response) => {
  const task = tasks.get(req.params.id);
  if (!task) return res.status(404).json({ error: 'Not found' });
  const updated = { ...task, ...req.body };
  tasks.set(task.id, updated);
  res.json(updated);
});

// DELETE /tasks/:id
router.delete('/:id', (req: Request, res: Response) => {
  if (!tasks.delete(req.params.id))
    return res.status(404).json({ error: 'Not found' });
  res.status(204).send();
});

export { router as tasksRouter, tasks };
07
Middleware & Error Handling
Express TypeScript
🇺🇦 Українська
Middleware — це функції, які виконуються між отриманням запиту і відправкою відповіді. Вони можуть логувати запити, перевіряти автентифікацію, трансформувати дані. Правильна обробка помилок — критична частина production-сервісу. Ми побудуємо кастомний клас AppError і централізований error handler.
🇬🇧 English
Middleware are functions that execute between receiving a request and sending a response. They can log requests, check authentication, transform data. Proper error handling is a critical part of a production service. We'll build a custom AppError class and centralized error handler.
src/middleware/errorHandler.tsTypeScript
import { Request, Response, NextFunction } from 'express';

export class AppError extends Error {
  constructor(
    public statusCode: number,
    public message: string,
    public isOperational = true
  ) {
    super(message);
    Object.setPrototypeOf(this, AppError.prototype);
  }
}

// Request logger middleware
export const requestLogger = (
  req: Request, _res: Response, next: NextFunction
) => {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
  next();
};

// Async wrapper — catches async errors automatically
export const asyncHandler = (
  fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
) => (req: Request, res: Response, next: NextFunction) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Global error handler — must have 4 parameters!
export const globalErrorHandler = (
  err: Error | AppError,
  _req: Request,
  res: Response,
  _next: NextFunction
) => {
  const statusCode = err instanceof AppError ? err.statusCode : 500;
  const isOperational = err instanceof AppError && err.isOperational;

  res.status(statusCode).json({
    error: {
      message: isOperational ? err.message : 'Internal Server Error',
      status: statusCode,
    }
  });
};
08
Request Validation with Zod
Zod Validation Jest
🇺🇦 Українська
Zod — це бібліотека валідації схем з першокласною підтримкою TypeScript. На відміну від Joi або Yup, Zod автоматично виводить TypeScript-типи зі схем. Ми створимо middleware для валідації body, params та query параметрів запитів.
🇬🇧 English
Zod is a schema validation library with first-class TypeScript support. Unlike Joi or Yup, Zod automatically infers TypeScript types from schemas. We'll create middleware to validate body, params, and query parameters of requests.
Install
PS> npm install zod
src/validation/schemas.tsTypeScript
import { z } from 'zod';

export const createUserSchema = z.object({
  body: z.object({
    name: z.string().min(2).max(100),
    email: z.string().email(),
    age: z.number().int().min(18).max(120).optional(),
    role: z.enum(['user', 'admin']).default('user'),
  }),
});

export type CreateUserInput = z.infer<typeof createUserSchema>['body'];

// Validation middleware factory
import { Request, Response, NextFunction } from 'express';
import { ZodSchema } from 'zod';

export const validate = (schema: ZodSchema) =>
  (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse({
      body: req.body,
      query: req.query,
      params: req.params,
    });
    if (!result.success) {
      return res.status(400).json({
        error: 'Validation failed',
        details: result.error.flatten(),
      });
    }
    next();
  };
09
Testing with Jest — Unit & Mocking
JestTypeScript
🇺🇦 Українська
Jest — найпопулярніший фреймворк для тестування в Node.js. Юніт-тести перевіряють окремі функції в ізоляції. Мокінг дозволяє замінити реальні залежності (БД, API) на підробки. Це дозволяє тестувати бізнес-логіку незалежно від зовнішніх систем.
🇬🇧 English
Jest is the most popular testing framework for Node.js. Unit tests check individual functions in isolation. Mocking lets you replace real dependencies (DB, API) with fakes. This allows testing business logic independently of external systems.
src/services/UserService.tsTypeScript
interface UserRepository {
  findById(id: string): Promise<{ id: string; name: string } | null>;
  create(data: { name: string; email: string }): Promise<{ id: string }>;
}

class UserService {
  constructor(private repo: UserRepository) {}

  async getUser(id: string) {
    const user = await this.repo.findById(id);
    if (!user) throw new Error('User not found');
    return user;
  }
}

export { UserService, UserRepository };
✓ Jest Mock Test — src/services/UserService.test.ts
import { UserService, UserRepository } from './UserService';

describe('UserService', () => {
  let mockRepo: jest.Mocked<UserRepository>;
  let service: UserService;

  beforeEach(() => {
    mockRepo = {
      findById: jest.fn(),
      create: jest.fn(),
    };
    service = new UserService(mockRepo);
  });

  it('returns user when found', async () => {
    mockRepo.findById.mockResolvedValue({ id: '1', name: 'Alice' });
    const user = await service.getUser('1');
    expect(user.name).toBe('Alice');
    expect(mockRepo.findById).toHaveBeenCalledWith('1');
  });

  it('throws when user not found', async () => {
    mockRepo.findById.mockResolvedValue(null);
    await expect(service.getUser('99')).rejects.toThrow('User not found');
  });
});
10
Integration Tests with Supertest
SupertestExpress
🇺🇦 Українська
Інтеграційні тести перевіряють, як компоненти працюють разом — роутер, middleware, сервіс. Supertest дозволяє надсилати HTTP-запити до Express-додатку без запуску реального сервера. Це ідеальний спосіб тестування API.
🇬🇧 English
Integration tests verify how components work together — router, middleware, service. Supertest lets you send HTTP requests to an Express app without starting a real server. This is the ideal way to test APIs.
✓ Integration Test — src/routes/tasks.test.ts
import request from 'supertest';
import express from 'express';
import { tasksRouter, tasks } from './tasks.router';

const app = express();
app.use(express.json());
app.use('/tasks', tasksRouter);

beforeEach(() => tasks.clear());

describe('Tasks API', () => {
  it('POST /tasks creates a task', async () => {
    const res = await request(app)
      .post('/tasks')
      .send({ title: 'Buy groceries' });
    expect(res.status).toBe(201);
    expect(res.body.title).toBe('Buy groceries');
    expect(res.body.done).toBe(false);
  });

  it('GET /tasks returns all tasks', async () => {
    await request(app).post('/tasks').send({ title: 'Task 1' });
    const res = await request(app).get('/tasks');
    expect(res.status).toBe(200);
    expect(res.body).toHaveLength(1);
  });

  it('DELETE /tasks/:id removes a task', async () => {
    const create = await request(app).post('/tasks').send({ title: 'Delete me' });
    const del = await request(app).delete(`/tasks/${create.body.id}`);
    expect(del.status).toBe(204);
  });
});
11
Environment Variables & Config
dotenvTypeScript
🇺🇦 Українська
Змінні середовища зберігають чутливі дані: паролі, API-ключі, URL бази даних. Пакет dotenv завантажує .env файл. Ми створимо типізований конфіг, який валідує всі обов'язкові змінні при старті.
🇬🇧 English
Environment variables store sensitive data: passwords, API keys, database URLs. The dotenv package loads .env files. We'll create a typed config that validates all required variables at startup.
src/config/env.tsTypeScript
import dotenv from 'dotenv';
import { z } from 'zod';

dotenv.config();

const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.string().transform(Number).default('3000'),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  JWT_EXPIRES_IN: z.string().default('7d'),
});

const parsed = envSchema.safeParse(process.env);
if (!parsed.success) {
  console.error('❌ Invalid environment variables:', parsed.error.format());
  process.exit(1);
}

export const env = parsed.data;
.envENV
NODE_ENV=development
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=your-super-secret-key-at-least-32-characters
JWT_EXPIRES_IN=7d
12
Logging with Winston
WinstonTypeScript
🇺🇦 Українська
Логування — критична частина production-сервісу. Winston — найпоширеніша бібліотека логування для Node.js. Вона підтримує різні рівні (error, warn, info, debug), транспорти (консоль, файл), та структуровані JSON-логи.
🇬🇧 English
Logging is a critical part of a production service. Winston is the most widely used logging library for Node.js. It supports different levels (error, warn, info, debug), transports (console, file), and structured JSON logs.
src/utils/logger.tsTypeScript
import winston from 'winston';
import { env } from '../config/env';

const logger = winston.createLogger({
  level: env.NODE_ENV === 'production' ? 'info' : 'debug',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      ),
    }),
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
  ],
});

export { logger };
13
PostgreSQL with pg Driver
PostgreSQLTypeScriptTesting
🇺🇦 Українська
PostgreSQL — найпотужніша відкрита реляційна СУБД. Пакет pg надає низькорівневий доступ до PostgreSQL. Ми створимо пул з'єднань, навчимось виконувати параметризовані запити та транзакції.
🇬🇧 English
PostgreSQL is the most powerful open-source relational DBMS. The pg package provides low-level access to PostgreSQL. We'll create a connection pool, learn to run parameterized queries and transactions.
src/db/pool.tsTypeScript
import { Pool } from 'pg';
import { env } from '../config/env';

export const pool = new Pool({
  connectionString: env.DATABASE_URL,
  max: 10,            // max connections in pool
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

// Generic query helper with types
export const query = <T = Record<string, unknown>>(
  text: string,
  values?: unknown[]
) => pool.query<T>(text, values);

// Transaction helper
export const withTransaction = async<T>(
  fn: (client: import('pg').PoolClient) => Promise<T>
): Promise<T> => {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    const result = await fn(client);
    await client.query('COMMIT');
    return result;
  } catch (err) {
    await client.query('ROLLBACK');
    throw err;
  } finally {
    client.release();
  }
};
14
ORM with Prisma
PrismaTypeScriptTesting
🇺🇦 Українська
Prisma — сучасний TypeScript ORM, що генерує типізований клієнт з схеми бази даних. Ви отримуєте автодоповнення для всіх запитів, автоматичну міграцію та Prisma Studio для перегляду даних.
🇬🇧 English
Prisma is a modern TypeScript ORM that generates a typed client from your database schema. You get autocomplete for all queries, automatic migrations, and Prisma Studio for browsing data.
Setup
PS> npm install prisma @prisma/client
PS> npx prisma init
PS> npx prisma migrate dev --name init
PS> npx prisma generate
prisma/schema.prismaPrisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        String   @id @default(cuid())
  name      String
  email     String   @unique
  password  String
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
}

enum Role { USER ADMIN }
src/db/prisma.tsTypeScript
import { PrismaClient } from '@prisma/client';

// Singleton pattern for Prisma
const globalForPrisma = global as typeof global & { prisma?: PrismaClient };

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: ['query', 'info', 'warn', 'error'],
});

if (process.env.NODE_ENV !== 'production')
  globalForPrisma.prisma = prisma;
15
Database Migrations Strategy
MigrationsPrisma
🇺🇦 Українська
Міграції бази даних — це версійний контроль для схеми БД. Кожна зміна структури таблиць записується у файл міграції. Це дозволяє відтворювати точний стан БД на будь-якому середовищі і легко відкочуватись до попередніх версій.
🇬🇧 English
Database migrations are version control for your DB schema. Every schema change is recorded in a migration file. This lets you reproduce the exact DB state on any environment and easily roll back to previous versions.
Prisma Migration Commands
PS> # Create a new migration
PS> npx prisma migrate dev --name add_posts_table

PS> # Apply migrations on production (no prompt)
PS> npx prisma migrate deploy

PS> # Reset DB (dev only!)
PS> npx prisma migrate reset

PS> # Seed the database
PS> npx prisma db seed
prisma/seed.tsTypeScript
import { prisma } from '../src/db/prisma';
import bcrypt from 'bcrypt';

async function main() {
  const hashedPwd = await bcrypt.hash('admin123', 10);
  await prisma.user.upsert({
    where: { email: 'admin@example.com' },
    update: {},
    create: { name: 'Admin', email: 'admin@example.com', password: hashedPwd, role: 'ADMIN' },
  });
  console.log('✅ Seed complete');
}

main().catch(console.error).finally(() => prisma.$disconnect());
16
Authentication with JWT
JWTTypeScriptTesting
🇺🇦 Українська
JWT (JSON Web Token) — стандарт безстанової автентифікації. Сервер підписує токен секретним ключем. Клієнт надсилає токен у заголовку Authorization. Ми реалізуємо повний flow: реєстрація, логін, захищені маршрути.
🇬🇧 English
JWT (JSON Web Token) is the stateless authentication standard. The server signs the token with a secret key. The client sends the token in the Authorization header. We'll implement the full flow: registration, login, protected routes.
src/auth/jwt.service.tsTypeScript
import jwt from 'jsonwebtoken';
import { env } from '../config/env';

interface TokenPayload { userId: string; role: string }

export const signToken = (payload: TokenPayload): string =>
  jwt.sign(payload, env.JWT_SECRET, { expiresIn: env.JWT_EXPIRES_IN });

export const verifyToken = (token: string): TokenPayload =>
  jwt.verify(token, env.JWT_SECRET) as TokenPayload;

// Auth middleware
import { Request, Response, NextFunction } from 'express';

export const authenticate = (req: Request, res: Response, next: NextFunction) => {
  const header = req.headers.authorization;
  if (!header?.startsWith('Bearer '))
    return res.status(401).json({ error: 'No token provided' });
  try {
    req.user = verifyToken(header.slice(7));
    next();
  } catch {
    res.status(401).json({ error: 'Invalid or expired token' });
  }
};
17
Password Hashing with bcrypt
Securitybcrypt
🇺🇦 Українська
Паролі ніколи не зберігаються у відкритому вигляді. bcrypt — алгоритм хешування, спеціально розроблений для паролів. Він повільний навмисно — це захищає від brute-force атак. Сіль (salt) додає унікальності кожному хешу.
🇬🇧 English
Passwords are never stored in plain text. bcrypt is a hashing algorithm specifically designed for passwords. It is intentionally slow — this protects against brute-force attacks. Salt adds uniqueness to every hash.
src/auth/password.service.tsTypeScript
import bcrypt from 'bcrypt';

const SALT_ROUNDS = 12; // higher = slower = safer

export const hashPassword = (plain: string): Promise<string> =>
  bcrypt.hash(plain, SALT_ROUNDS);

export const verifyPassword = (plain: string, hashed: string): Promise<boolean> =>
  bcrypt.compare(plain, hashed);
✓ Test — password.service.test.ts
import { hashPassword, verifyPassword } from './password.service';

describe('PasswordService', () => {
  it('hashes and verifies correctly', async () => {
    const hash = await hashPassword('secret123');
    expect(hash).not.toBe('secret123');
    expect(await verifyPassword('secret123', hash)).toBe(true);
    expect(await verifyPassword('wrong', hash)).toBe(false);
  });
});
18
Role-Based Access Control (RBAC)
RBACTypeScript
🇺🇦 Українська
RBAC — система контролю доступу на основі ролей. Кожен користувач має роль (user, admin, moderator), а кожен маршрут вимагає певних прав. Ми побудуємо гнучкий middleware для перевірки ролей.
🇬🇧 English
RBAC is a role-based access control system. Each user has a role (user, admin, moderator) and each route requires certain permissions. We'll build a flexible role-checking middleware.
src/middleware/authorize.tsTypeScript
import { Request, Response, NextFunction } from 'express';

type Role = 'USER' | 'ADMIN' | 'MODERATOR';

export const authorize = (...roles: Role[]) =>
  (req: Request, res: Response, next: NextFunction) => {
    if (!req.user) return res.status(401).json({ error: 'Unauthenticated' });
    if (!roles.includes(req.user.role as Role))
      return res.status(403).json({ error: 'Insufficient permissions' });
    next();
  };

// Usage in routes:
// router.delete('/users/:id', authenticate, authorize('ADMIN'), deleteUser);
19
File Upload Service with Multer
MulterTypeScript
🇺🇦 Українська
Multer — middleware для обробки multipart/form-data, яке використовується для завантаження файлів. Ми налаштуємо обмеження розміру файлу, фільтр типів та зберігання на диск або в пам'ять.
🇬🇧 English
Multer is middleware for handling multipart/form-data, used for file uploads. We'll configure file size limits, type filters, and disk or memory storage.
src/middleware/upload.tsTypeScript
import multer from 'multer';
import path from 'path';

const storage = multer.diskStorage({
  destination: (_req, _file, cb) => cb(null, 'uploads/'),
  filename: (_req, file, cb) => {
    const ext = path.extname(file.originalname);
    cb(null, `${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`);
  },
});

const fileFilter = (
  _req: any, file: Express.Multer.File,
  cb: multer.FileFilterCallback
) => {
  const allowed = ['image/jpeg', 'image/png', 'image/webp'];
  if (allowed.includes(file.mimetype)) cb(null, true);
  else cb(new Error('Only JPEG, PNG, WEBP allowed'));
};

export const upload = multer({
  storage,
  fileFilter,
  limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
});
20
Async Patterns, Promises & Event Emitters
AsyncTypeScript
🇺🇦 Українська
Node.js побудований на асинхронній моделі виконання. Розуміння Promise.all, Promise.race, EventEmitter та async generators є критичним для написання ефективного серверного коду.
🇬🇧 English
Node.js is built on an asynchronous execution model. Understanding Promise.all, Promise.race, EventEmitter and async generators is critical for writing efficient server code.
src/utils/async.tsTypeScript
import { EventEmitter } from 'events';

// Run tasks in parallel with typed results
export const parallel = <T>(tasks: Promise<T>[]): Promise<T[]> =>
  Promise.all(tasks);

// Run with concurrency limit
export async function withConcurrency<T>(
  items: T[],
  limit: number,
  fn: (item: T) => Promise<void>
): Promise<void> {
  const chunks: T[][] = [];
  for (let i = 0; i < items.length; i += limit)
    chunks.push(items.slice(i, i + limit));
  for (const chunk of chunks)
    await Promise.all(chunk.map(fn));
}

// Typed EventEmitter
interface AppEvents {
  userCreated: [{ id: string; email: string }];
  paymentReceived: [{ amount: number; userId: string }];
}

export const appEvents = new EventEmitter() as EventEmitter & {
  on<K extends keyof AppEvents>(ev: K, fn: (...args: AppEvents[K]) => void): any;
  emit<K extends keyof AppEvents>(ev: K, ...args: AppEvents[K]): boolean;
};
21
Redis Caching Layer
RedisTypeScriptTesting
🇺🇦 Українська
Redis — in-memory сховище даних, ідеальне для кешування. Кешування зменшує навантаження на БД та пришвидшує відповіді. Ми реалізуємо cache-aside патерн: спочатку перевіряємо кеш, якщо немає — запитуємо БД і кешуємо результат.
🇬🇧 English
Redis is an in-memory data store, ideal for caching. Caching reduces database load and speeds up responses. We'll implement the cache-aside pattern: first check cache, if miss — query DB and cache the result.
src/cache/redis.service.tsTypeScript
import { createClient } from 'redis';

const client = createClient({ url: process.env.REDIS_URL });
await client.connect();

export class CacheService {
  async get<T>(key: string): Promise<T | null> {
    const value = await client.get(key);
    return value ? JSON.parse(value) : null;
  }

  async set<T>(key: string, value: T, ttlSeconds = 3600): Promise<void> {
    await client.set(key, JSON.stringify(value), { EX: ttlSeconds });
  }

  async del(key: string): Promise<void> {
    await client.del(key);
  }

  // Cache-aside pattern
  async remember<T>(
    key: string,
    ttl: number,
    fn: () => Promise<T>
  ): Promise<T> {
    const cached = await this.get<T>(key);
    if (cached) return cached;
    const value = await fn();
    await this.set(key, value, ttl);
    return value;
  }
}
22
WebSockets with socket.io
WebSocketTypeScript
🇺🇦 Українська
WebSocket — протокол двонаправленого зв'язку між клієнтом і сервером. Socket.io спрощує роботу з WebSockets, додаючи кімнати, нміспейси та автоматичне перепідключення. Ідеально для чатів, live-оновлень, ігор.
🇬🇧 English
WebSocket is a bidirectional communication protocol between client and server. Socket.io simplifies WebSockets adding rooms, namespaces and auto-reconnect. Perfect for chats, live updates, games.
src/websocket/chat.gateway.tsTypeScript
import { Server, Socket } from 'socket.io';

interface ChatMessage {
  room: string; message: string; user: string;
}

export const setupChatGateway = (io: Server) => {
  io.on('connection', (socket: Socket) => {
    console.log(`Client connected: ${socket.id}`);

    socket.on('join-room', (room: string) => {
      socket.join(room);
      socket.to(room).emit('user-joined', { socketId: socket.id });
    });

    socket.on('send-message', (data: ChatMessage) => {
      io.to(data.room).emit('new-message', {
        ...data,
        timestamp: new Date().toISOString(),
      });
    });

    socket.on('disconnect', () => {
      console.log(`Client disconnected: ${socket.id}`);
    });
  });
};
23
Message Queues with BullMQ
BullMQRedisTypeScript
🇺🇦 Українська
Черги повідомлень дозволяють виконувати важкі задачі асинхронно: обробка зображень, відправка email, генерація звітів. BullMQ побудована на Redis і підтримує retry-логіку, пріоритети і планування задач.
🇬🇧 English
Message queues allow heavy tasks to run asynchronously: image processing, sending emails, generating reports. BullMQ is built on Redis and supports retry logic, priorities, and scheduled jobs.
src/queue/emailQueue.tsTypeScript
import { Queue, Worker } from 'bullmq';

interface EmailJobData {
  to: string; subject: string; body: string;
}

const connection = { host: 'localhost', port: 6379 };

export const emailQueue = new Queue<EmailJobData>('email', { connection });

export const emailWorker = new Worker<EmailJobData>(
  'email',
  async (job) => {
    console.log(`Sending email to ${job.data.to}: ${job.data.subject}`);
    // await sendEmail(job.data);
  },
  {
    connection,
    attempts: 3,        // retry 3 times on failure
    backoff: { type: 'exponential', delay: 1000 },
  }
);

// Add job to queue
export const queueEmail = (data: EmailJobData) =>
  emailQueue.add('send', data, { delay: 0 });
24
Email Service with Nodemailer
NodemailerTypeScript
🇺🇦 Українська
Nodemailer — найпопулярніша бібліотека для відправки email в Node.js. Підтримує SMTP, OAuth2, та HTML-шаблони. Ми побудуємо сервіс з підтримкою транзакційних email (підтвердження, скидання паролю).
🇬🇧 English
Nodemailer is the most popular email-sending library for Node.js. It supports SMTP, OAuth2, and HTML templates. We'll build a service supporting transactional emails (confirmation, password reset).
src/services/email.service.tsTypeScript
import nodemailer from 'nodemailer';

const transporter = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: Number(process.env.SMTP_PORT),
  secure: true,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASS,
  },
});

export class EmailService {
  async sendWelcome(to: string, name: string) {
    await transporter.sendMail({
      from: '"My App" <noreply@myapp.com>',
      to,
      subject: 'Welcome!',
      html: `<h1>Hello ${name}!</h1><p>Thanks for registering.</p>`,
    });
  }

  async sendPasswordReset(to: string, token: string) {
    const resetUrl = `https://myapp.com/reset?token=${token}`;
    await transporter.sendMail({
      from: '"My App" <noreply@myapp.com>',
      to,
      subject: 'Password Reset',
      html: `<p>Reset your password: <a href="${resetUrl}">Click here</a></p>`,
    });
  }
}
25
API Rate Limiting
Securityexpress-rate-limit
🇺🇦 Українська
Rate limiting обмежує кількість запитів з однієї IP-адреси за певний проміжок часу. Це захищає API від DDoS атак, brute-force та зловживань. Ми налаштуємо різні ліміти для різних маршрутів.
🇬🇧 English
Rate limiting restricts the number of requests from a single IP address within a time window. This protects the API from DDoS attacks, brute-force and abuse. We'll configure different limits for different routes.
src/middleware/rateLimiter.tsTypeScript
import rateLimit from 'express-rate-limit';

export const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,               // 100 requests per window
  message: { error: 'Too many requests' },
  standardHeaders: true,
  legacyHeaders: false,
});

export const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 hour
  max: 10,                  // 10 login attempts
  message: { error: 'Too many login attempts, try again in 1 hour' },
});

// Apply in app.ts:
// app.use(globalLimiter);
// app.post('/auth/login', authLimiter, loginHandler);
26
Dockerizing Node.js on Windows
DockerTypeScript
🇺🇦 Українська
Docker дозволяє упакувати додаток разом з усіма залежностями у контейнер. На Windows потрібен Docker Desktop. Ми створимо оптимальний Dockerfile для production і docker-compose для локальної розробки.
🇬🇧 English
Docker lets you package an application with all its dependencies into a container. On Windows you need Docker Desktop. We'll create an optimized production Dockerfile and docker-compose for local development.
DockerfileDockerfile
# Multi-stage build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:20-alpine AS production
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
docker-compose.ymlYAML
version: '3.9'
services:
  app:
    build: .
    ports: ['3000:3000']
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/mydb
      - REDIS_URL=redis://redis:6379
    depends_on: [db, redis]

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: mydb
    volumes: [postgres_data:/var/lib/postgresql/data]

  redis:
    image: redis:7-alpine

volumes:
  postgres_data:
27
CI/CD with GitHub Actions
CI/CDGitHub
🇺🇦 Українська
CI/CD автоматизує тестування і деплой при кожному push у репозиторій. GitHub Actions дозволяє запускати workflows прямо в GitHub. Ми налаштуємо pipeline: lint → test → build → deploy.
🇬🇧 English
CI/CD automates testing and deployment on every push to the repository. GitHub Actions lets you run workflows directly in GitHub. We'll set up a pipeline: lint → test → build → deploy.
.github/workflows/ci.ymlYAML
name: CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: testdb
        ports: ['5432:5432']

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm run lint
      - run: npm test
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/testdb
      - run: npm run build
28
Performance & Profiling
PerformanceTypeScript
🇺🇦 Українська
Продуктивність — ключова метрика production-сервісу. Node.js надає вбудований profiler. Ми дізнаємось, як знаходити memory leaks за допомогою heapdump, вимірювати затримки запитів та оптимізувати N+1 запити до БД.
🇬🇧 English
Performance is a key metric of a production service. Node.js provides a built-in profiler. We'll learn how to find memory leaks using heapdump, measure request latencies, and optimize N+1 database queries.
src/middleware/metrics.tsTypeScript
import { Request, Response, NextFunction } from 'express';

const metrics: Record<string, { count: number; totalMs: number }> = {};

export const metricsMiddleware = (
  req: Request, res: Response, next: NextFunction
) => {
  const start = process.hrtime.bigint();

  res.on('finish', () => {
    const durationMs = Number(process.hrtime.bigint() - start) / 1e6;
    const key = `${req.method} ${req.route?.path || req.path}`;
    if (!metrics[key]) metrics[key] = { count: 0, totalMs: 0 };
    metrics[key].count++;
    metrics[key].totalMs += durationMs;
    res.setHeader('X-Response-Time', `${durationMs.toFixed(2)}ms`);
  });
  next();
};

export const getMetrics = () =>
  Object.entries(metrics).map(([route, data]) => ({
    route,
    count: data.count,
    avgMs: (data.totalMs / data.count).toFixed(2),
  }));
29
GraphQL API with Apollo Server
GraphQLTypeScriptTesting
🇺🇦 Українська
GraphQL — мова запитів для API, розроблена Facebook. На відміну від REST, клієнт точно вказує, які поля йому потрібні. Apollo Server — найпопулярніша реалізація GraphQL для Node.js з TypeScript.
🇬🇧 English
GraphQL is a query language for APIs, developed by Facebook. Unlike REST, clients specify exactly which fields they need. Apollo Server is the most popular GraphQL implementation for Node.js with TypeScript.
src/graphql/schema.tsTypeScript
import { gql } from 'graphql-tag';

export const typeDefs = gql`
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    author: User!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
  }

  type Mutation {
    createUser(name: String!, email: String!): User!
    deleteUser(id: ID!): Boolean!
  }
`;

export const resolvers = {
  Query: {
    users: (_: unknown, __: unknown, { prisma }: any) =>
      prisma.user.findMany(),
    user: (_: unknown, { id }: { id: string }, { prisma }: any) =>
      prisma.user.findUnique({ where: { id } }),
  },
  User: {
    posts: (parent: { id: string }, _: unknown, { prisma }: any) =>
      prisma.post.findMany({ where: { authorId: parent.id } }),
  },
};
30
Microservices Architecture
MicroservicesTypeScriptArchitecture
🇺🇦 Українська
Мікросервіси — архітектурний підхід, де додаток розбивається на невеликі незалежні сервіси, кожен з яких відповідає за одну задачу. Вони спілкуються через HTTP або черги повідомлень. API Gateway є точкою входу для всіх запитів. Це фінальний урок — ти вже backend developer!
🇬🇧 English
Microservices is an architectural approach where an application is split into small independent services, each responsible for one task. They communicate via HTTP or message queues. API Gateway is the entry point for all requests. This is the final lesson — you are now a backend developer!
Architecture Overview
API Gateway
Routes requests to appropriate microservices, handles auth
User Service
Registration, login, profile management
Order Service
Create/track orders, inventory
Notification Service
Email/SMS via BullMQ queue
Message Bus
Redis Pub/Sub or RabbitMQ between services
Service Discovery
Each service registers itself; gateway routes dynamically
src/gateway/proxy.tsTypeScript
import express from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';

const app = express();

const services: Record<string, string> = {
  '/api/users':  'http://user-service:3001',
  '/api/orders': 'http://order-service:3002',
  '/api/notify': 'http://notification-service:3003',
};

Object.entries(services).forEach(([path, target]) => {
  app.use(path, createProxyMiddleware({ target, changeOrigin: true }));
});

app.listen(3000, () => console.log('🚀 API Gateway running on :3000'));
🎉 Congratulations! / Вітаємо!

You have completed all 30 lessons of the Backend Mastery course. You now know: TypeScript, Node.js, Express, REST APIs, Authentication, PostgreSQL, Prisma, Redis, WebSockets, Message Queues, Docker, CI/CD, GraphQL, and Microservices.

Ви пройшли всі 30 уроків курсу Backend Mastery. Тепер ви знаєте: TypeScript, Node.js, Express, REST API, автентифікацію, PostgreSQL, Prisma, Redis, WebSockets, черги повідомлень, Docker, CI/CD, GraphQL та мікросервіси.