Что такое Express.js

Зачем нужно

Express -- минималистичный веб-фреймворк для Node.js. Он оборачивает встроенный модуль http удобным API: роутинг, middleware, обработка ошибок, раздача статики. Вместо ручного парсинга URL и метода в http.createServer Express даёт app.get('/users', handler). Это самый популярный фреймворк Node.js (~30M скачиваний в неделю).

Где используется

  • REST API (бэкенд для SPA, мобильных приложений)
  • Server-side rendering (с EJS, Pug, Handlebars)
  • Микросервисы и BFF (Backend For Frontend)
  • Прокси-серверы и API-гейтвеи
  • Прототипирование и MVP
  • Фундамент для Nest.js (под капотом Express или Fastify)

Предпосылки

  • http — как работает HTTP-сервер в Node.js
  • npm basics — установка пакетов
  • package.json — структура проекта

Установка

# Создать проект
mkdir my-api && cd my-api
npm init -y

# Установить Express
npm install express

Hello World

const express = require('express');
const app = express;

// Обработка GET-запроса на /
app.get('/', (req, res) => {
  res.send('Hello, Express!');
});

// Запуск сервера
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running: http://localhost:${PORT}`);
});
node app.js
# Server running: http://localhost:3000

curl http://localhost:3000
# Hello, Express!

Сравнение: http vs Express

// === Чистый http ===
const http = require('http');

http.createServer((req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);

  if (req.method === 'GET' && url.pathname === '/api/users') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify([{ id: 1, name: 'Anna' }]));
  } else if (req.method === 'POST' && url.pathname === '/api/users') {
    let body = '';
    req.on('data', chunk => body += chunk);
    req.on('end', () => {
      const user = JSON.parse(body);
      res.writeHead(201, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(user));
    });
  } else {
    res.writeHead(404);
    res.end('Not Found');
  }
}).listen(3000);

// === Express ===
const express = require('express');
const app = express;

app.use(express.json());  // парсинг JSON-тела

app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'Anna' }]);
});

app.post('/api/users', (req, res) => {
  res.status(201).json(req.body);
});

app.listen(3000);

Express убирает повторяющийся код: парсинг URL, методов, тела запроса, Content-Type.


Объекты req и res в Express

req — расширенный request

app.get('/api/users/:id', (req, res) => {
  // Параметры маршрута
  console.log(req.params);        // { id: '42' }
  console.log(req.params.id);     // '42'

  // Query-параметры (?page=2&limit=10)
  console.log(req.query);         // { page: '2', limit: '10' }
  console.log(req.query.page);    // '2'

  // Заголовки
  console.log(req.headers);       // { 'content-type': '...', ... }
  console.log(req.get('Content-Type')); // 'application/json'

  // Метод и путь
  console.log(req.method);        // 'GET'
  console.log(req.path);          // '/api/users/42'
  console.log(req.originalUrl);   // '/api/users/42?page=2'

  // Тело запроса (после express.json() middleware)
  console.log(req.body);          // { name: 'Anna' } (для POST/PUT)

  // IP клиента
  console.log(req.ip);            // '127.0.0.1'
});

res — расширенный response

app.get('/api/data', (req, res) => {
  // JSON-ответ (автоматически Content-Type: application/json)
  res.json({ status: 'ok', data: [1, 2, 3] });

  // Текстовый ответ
  res.send('Hello!');

  // HTML
  res.send('<h1>Hello</h1>');

  // Установка статус-кода
  res.status(201).json({ id: 1 });
  res.status(404).json({ error: 'Not Found' });
  res.status(204).end(); // No Content, без тела

  // Редирект
  res.redirect('/new-url');
  res.redirect(301, '/permanent-new-url');

  // Заголовки
  res.set('X-Custom-Header', 'value');
  res.set({
    'X-Powered-By': 'My Server',
    'Cache-Control': 'no-cache'
  });

  // Скачивание файла
  res.download('/path/to/file.pdf');

  // Отправка файла
  res.sendFile('/absolute/path/to/file.html');
});

Концепция Middleware

Middleware -- функция, имеющая доступ к req, res и next. Каждый запрос проходит через цепочку middleware-ов, как через конвейер.

Запрос → [Middleware 1] → [Middleware 2] → [Route Handler] → Ответ

Каждый middleware может:
  1. Изменить req/res
  2. Завершить запрос (res.send)
  3. Передать управление дальше (next)
const express = require('express');
const app = express;

// Middleware 1: логирование
app.use((req, res, next) => {
  console.log(`${new Date.toISOString()} ${req.method} ${req.url}`);
  next; // передать следующему middleware
});

// Middleware 2: парсинг JSON-тела
app.use(express.json());

// Middleware 3: CORS
app.use((req, res, next) => {
  res.set('Access-Control-Allow-Origin', '*');
  next;
});

// Route handler (конечный обработчик)
app.get('/api/users', (req, res) => {
  res.json([{ id: 1, name: 'Anna' }]);
});

app.listen(3000);

Request-Response цикл

Клиент отправляет HTTP-запрос
         │
         ▼
┌─────────────────────────┐
│   express.json()         │  ← Парсит тело запроса
└────────┬────────────────┘
         │ next
         ▼
┌─────────────────────────┐
│   Логирование            │  ← Записывает запрос в лог
└────────┬────────────────┘
         │ next
         ▼
┌─────────────────────────┐
│   Аутентификация         │  ← Проверяет JWT-токен
└────────┬────────────────┘
         │ next
         ▼
┌─────────────────────────┐
│   Route handler          │  ← Бизнес-логика + res.json()
└────────┬────────────────┘
         │
         ▼
Клиент получает HTTP-ответ

Если на любом шаге вызов next пропущен
и res.send/json не вызван — запрос "зависнет"

Структура проекта

my-api/
├── src/
│   ├── app.js              ← создание Express-приложения
│   ├── server.js           ← запуск сервера (app.listen)
│   ├── routes/
│   │   ├── users.js        ← /api/users
│   │   └── posts.js        ← /api/posts
│   ├── middleware/
│   │   ├── auth.js         ← проверка аутентификации
│   │   └── errorHandler.js ← обработка ошибок
│   └── controllers/
│       ├── userController.js
│       └── postController.js
├── package.json
├── .env
└── .gitignore
// src/app.js
const express = require('express');
const cors = require('cors');
const userRoutes = require('./routes/users');
const postRoutes = require('./routes/posts');
const errorHandler = require('./middleware/errorHandler');

const app = express;

// Глобальные middleware
app.use(cors);
app.use(express.json());

// Маршруты
app.use('/api/users', userRoutes);
app.use('/api/posts', postRoutes);

// Обработка ошибок (всегда последний middleware)
app.use(errorHandler);

module.exports = app;

// src/server.js
const app = require('./app');
const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});

Полный пример: минимальный API

const express = require('express');
const app = express;

app.use(express.json());

// Хранилище (для примера — в памяти)
let todos = [
  { id: 1, text: 'Learn Express', done: false },
  { id: 2, text: 'Build API', done: false }
];
let nextId = 3;

// GET /api/todos — все задачи
app.get('/api/todos', (req, res) => {
  res.json(todos);
});

// GET /api/todos/:id — одна задача
app.get('/api/todos/:id', (req, res) => {
  const todo = todos.find(t => t.id === Number(req.params.id));
  if (!todo) {
    return res.status(404).json({ error: 'Todo not found' });
  }
  res.json(todo);
});

// POST /api/todos — создать задачу
app.post('/api/todos', (req, res) => {
  const { text } = req.body;
  if (!text) {
    return res.status(400).json({ error: 'Text is required' });
  }
  const todo = { id: nextId++, text, done: false };
  todos.push(todo);
  res.status(201).json(todo);
});

// PATCH /api/todos/:id — обновить задачу
app.patch('/api/todos/:id', (req, res) => {
  const todo = todos.find(t => t.id === Number(req.params.id));
  if (!todo) {
    return res.status(404).json({ error: 'Todo not found' });
  }
  Object.assign(todo, req.body);
  res.json(todo);
});

// DELETE /api/todos/:id — удалить задачу
app.delete('/api/todos/:id', (req, res) => {
  const index = todos.findIndex(t => t.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: 'Todo not found' });
  }
  todos.splice(index, 1);
  res.status(204).end();
});

app.listen(3000, () => {
  console.log('Todo API: http://localhost:3000/api/todos');
});

Частые ошибки

  1. Забывают express.jsonreq.body будет undefined для POST/PUT запросов
  2. Нет return перед res.json() — код продолжает выполняться после ответа, вызывая двойной ответ
  3. Забывают next в middleware — запрос зависает, клиент не получает ответ
  4. Порядок middlewareapp.use(express.json()) должен быть ДО route handler-ов
  5. Обработка ошибок не последняя — error handler middleware должен быть определён после всех маршрутов
  6. Хардкод портаapp.listen(3000) вместо process.env.PORT || 3000

Практика

  1. Создать Express-сервер с app.get('/'), возвращающий JSON
  2. Добавить express.json и создать POST-эндпоинт, принимающий данные
  3. Реализовать полный CRUD API для ресурса (GET, POST, PATCH, DELETE)
  4. Добавить middleware для логирования каждого запроса (метод, URL, время)
  5. Настроить переменную PORT через process.env

Связанные темы

  • http — чистый HTTP-сервер (без Express)
  • Роутинг — маршруты и параметры
  • Middleware — цепочка middleware

Ресурсы