Модуль http — создание сервера

Зачем нужно

Модуль http -- встроенный инструмент Node.js для создания HTTP-серверов и выполнения HTTP-запросов. Это фундамент, на котором построены Express, Fastify, Koa и все остальные веб-фреймворки. Понимание http модуля необходимо для отладки, оптимизации и создания легковесных серверов без фреймворков.

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

  • Простые API-серверы и микросервисы
  • Health-check эндпоинты
  • Прокси-серверы
  • Раздача статических файлов
  • Webhook-обработчики
  • Фундамент для Express, Fastify, Koa

Предпосылки

  • Что такое Node.js — событийная модель
  • Stream API -- потоки — req и res являются потоками
  • Базовое понимание HTTP-протокола (методы, статус-коды, заголовки)

Минимальный сервер

const http = require('http');

const server = http.createServer((req, res) => {
  // req — IncomingMessage (Readable Stream)
  // res — ServerResponse (Writable Stream)

  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  res.end('Привет, мир!');
});

server.listen(3000, () => {
  console.log('Сервер запущен: http://localhost:3000');
});
node server.js
# Сервер запущен: http://localhost:3000

curl http://localhost:3000
# Привет, мир!

Объект request (req)

const http = require('http');

const server = http.createServer((req, res) => {
  // Основные свойства запроса
  console.log(req.method);       // 'GET', 'POST', 'PUT', 'DELETE'
  console.log(req.url);          // '/api/users?page=2'
  console.log(req.headers);      // { 'content-type': '...', host: '...' }
  console.log(req.httpVersion);  // '1.1'

  // Парсинг URL
  const url = new URL(req.url, `http://${req.headers.host}`);
  console.log(url.pathname);     // '/api/users'
  console.log(url.searchParams.get('page')); // '2'

  res.end('OK');
});

server.listen(3000);

Объект response (res)

const http = require('http');

const server = http.createServer((req, res) => {
  // Установка статус-кода
  res.statusCode = 200;

  // Установка заголовков
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
  res.setHeader('X-Request-Id', '12345');
  res.setHeader('Cache-Control', 'no-cache');

  // Или всё сразу через writeHead
  res.writeHead(200, {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  });

  // Отправка тела ответа
  res.write('Часть 1');     // можно вызывать несколько раз
  res.write('Часть 2');
  res.end('Конец');          // завершает ответ (обязательно)

  // Или сразу всё
  res.end(JSON.stringify({ status: 'ok' }));
});

server.listen(3000);

Основные статус-коды

// 2xx — Успех
200  // OK
201  // Created (POST — ресурс создан)
204  // No Content (DELETE — тело пустое)

// 3xx — Перенаправление
301  // Moved Permanently
302  // Found (временный редирект)
304  // Not Modified

// 4xx — Ошибка клиента
400  // Bad Request
401  // Unauthorized (не аутентифицирован)
403  // Forbidden (нет прав)
404  // Not Found
405  // Method Not Allowed
429  // Too Many Requests

// 5xx — Ошибка сервера
500  // Internal Server Error
502  // Bad Gateway
503  // Service Unavailable

Простой роутинг

const http = require('http');

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

  // JSON-ответ
  function json(statusCode, data) {
    res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify(data));
  }

  // Роутинг
  if (method === 'GET' && pathname === '/') {
    json(200, { message: 'Welcome to API' });

  } else if (method === 'GET' && pathname === '/api/users') {
    const users = [
      { id: 1, name: 'Anna' },
      { id: 2, name: 'Boris' }
    ];
    json(200, users);

  } else if (method === 'GET' && pathname.startsWith('/api/users/')) {
    const id = pathname.split('/')[3];
    json(200, { id: Number(id), name: 'User ' + id });

  } else if (method === 'POST' && pathname === '/api/users') {
    // Обработка POST-данных — см. следующий раздел
    handlePostData(req, res);

  } else {
    json(404, { error: 'Not Found' });
  }
});

server.listen(3000, () => {
  console.log('API: http://localhost:3000');
});

Обработка POST-данных

const http = require('http');

// req — это Readable Stream, данные приходят чанками
function parseBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = ;

    req.on('data', (chunk) => {
      chunks.push(chunk);
    });

    req.on('end', () => {
      const body = Buffer.concat(chunks).toString();
      try {
        resolve(JSON.parse(body));
      } catch {
        resolve(body); // не JSON — вернуть как строку
      }
    });

    req.on('error', reject);
  });
}

const server = http.createServer(async (req, res) => {
  if (req.method === 'POST' && req.url === '/api/users') {
    try {
      const body = await parseBody(req);
      console.log('Получено:', body);
      // { name: 'Charlie', email: 'charlie@example.com' }

      res.writeHead(201, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({
        id: Date.now(),
        ...body
      }));
    } catch (err) {
      res.writeHead(400, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify({ error: 'Invalid JSON' }));
    }
  } else {
    res.writeHead(404);
    res.end('Not Found');
  }
});

server.listen(3000);
# Тестирование POST
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "Charlie", "email": "charlie@example.com"}'

Раздача статических файлов

const http = require('http');
const fs = require('fs');
const path = require('path');

const MIME_TYPES = {
  '.html': 'text/html',
  '.css':  'text/css',
  '.js':   'application/javascript',
  '.json()': 'application/json',
  '.png':  'image/png',
  '.jpg':  'image/jpeg',
  '.gif':  'image/gif',
  '.svg':  'image/svg+xml',
  '.ico':  'image/x-icon',
  '.txt':  'text/plain',
};

const PUBLIC_DIR = path.join(__dirname, 'public');

const server = http.createServer((req, res) => {
  // Защита от path traversal
  const safePath = path.normalize(req.url).replace(/^(\.\.[/\\])+/, '');
  let filePath = path.join(PUBLIC_DIR, safePath);

  // Для '/' отдаём index.html
  if (safePath === '/' || safePath === '\\') {
    filePath = path.join(PUBLIC_DIR, 'index.html');
  }

  const ext = path.extname(filePath);
  const contentType = MIME_TYPES[ext] || 'application/octet-stream';

  // Стримим файл (не загружаем целиком в память)
  const stream = fs.createReadStream(filePath);

  stream.on('open', () => {
    res.writeHead(200, { 'Content-Type': contentType });
    stream.pipe(res);
  });

  stream.on('error', (err) => {
    if (err.code === 'ENOENT') {
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('404 Not Found');
    } else {
      res.writeHead(500);
      res.end('Internal Server Error');
    }
  });
});

server.listen(3000, () => {
  console.log('Static server: http://localhost:3000');
});

Заголовки и CORS

const http = require('http');

const server = http.createServer((req, res) => {
  // CORS-заголовки (разрешить запросы с других доменов)
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  // Preflight-запрос (браузер отправляет OPTIONS перед POST/PUT)
  if (req.method === 'OPTIONS') {
    res.writeHead(204);
    res.end();
    return;
  }

  // Основная логика
  res.writeHead(200, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify({ data: 'response' }));
});

server.listen(3000);

События сервера

const http = require('http');

const server = http.createServer((req, res) => {
  res.end('OK');
});

// Сервер запущен
server.on('listening', () => {
  const addr = server.address;
  console.log(`Listening on ${addr.address}:${addr.port}`);
});

// Ошибка (порт занят и т.д.)
server.on('error', (err) => {
  if (err.code === 'EADDRINUSE') {
    console.error(`Порт ${err.port} уже занят`);
    process.exit(1);
  }
});

// Новое соединение
server.on('connection', (socket) => {
  console.log('New TCP connection from', socket.remoteAddress);
});

// Корректное завершение
process.on('SIGTERM', () => {
  server.close(() => {
    console.log('Server closed gracefully');
    process.exit(0);
  });
});

server.listen(3000);

Выполнение HTTP-запросов (клиент)

const http = require('http');
const https = require('https');

// GET-запрос
https.get('https://jsonplaceholder.typicode.com/todos/1', (res) => {
  let data = '';
  res.on('data', (chunk) => data += chunk);
  res.on('end', () => {
    console.log(JSON.parse(data));
  });
}).on('error', console.error);

// POST-запрос
const postData = JSON.stringify({ title: 'Test', body: 'Content' });

const options = {
  hostname: 'jsonplaceholder.typicode.com',
  port: 443,
  path: '/posts',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Content-Length': Buffer.byteLength(postData)
  }
};

const req = https.request(options, (res) => {
  let data = '';
  res.on('data', (chunk) => data += chunk);
  res.on('end', () => console.log(JSON.parse(data)));
});

req.on('error', console.error);
req.write(postData);
req.end();

// Рекомендация: для HTTP-клиента лучше использовать
// встроенный fetch (Node 18+) или библиотеку (axios, undici)

fetch (Node.js 18+)

// Встроенный fetch — проще, чем http.request
const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
const data = await response.json();
console.log(data);

// POST
const res = await fetch('https://jsonplaceholder.typicode.com/posts', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ title: 'Test' })
});

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

  1. Забывают res.end() — клиент ждёт ответ бесконечно (hang), соединение не закрывается
  2. Двойной ответres.end() вызван дважды (например, в callback и после него) → ERR_HTTP_HEADERS_SENT
  3. Не обрабатывают POST-телоreq.body не существует в чистом http, данные приходят через Stream
  4. writeHead после write — заголовки нужно отправить ДО тела ответа
  5. Игнорируют CORS — браузер блокирует запросы с другого домена без CORS-заголовков
  6. Синхронное чтение файловreadFileSync в обработчике запроса блокирует весь сервер

Практика

  1. Создать HTTP-сервер, отвечающий JSON на GET /api/time с текущим временем
  2. Реализовать POST /api/echo, который возвращает полученные данные обратно
  3. Написать простой static-файл-сервер с корректными MIME-типами
  4. Добавить роутинг: GET, POST, PUT, DELETE для ресурса /api/todos
  5. Реализовать graceful shutdown через SIGTERM

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

Ресурсы


🎓 Источник: HTTP сервер на Node.js (routing, cluster, IP sticky)

  • 📅 2018-10-17 · YouTube · [Marp](../../../Documents/TimurShemsedinov/2018-10-17 — HTTP сервер на Node.js (routing, cluster, IP sticky) (7Ufxj0oTaUo).md)
  • Тезисы:
    • req и res — стримы; парсим тело через data/end события или утилиты типа body-parser
    • Свой роутинг строится через req.method + req.url + map
    • res.writeHead(200, {...}) + res.end(body) — минимальный ответ
    • Cluster распределяет соединения round-robin (Linux) или OS-зависимо (Windows)
    • IP sticky для сессий: один и тот же IP всегда попадает на один worker (если нужно)
    • Без фреймворка можно отдавать 20–50k req/s на одном процессе

🎓 Источник: Node.js HTTP Proxy — ревью кода

  • 📅 2023-11-29 · YouTube
  • Тезисы: разбор реализации proxy на чистой ноде, ошибки кандидата, как правильно

🎓 Источник: Обзор встроенного Node.js API

  • 📅 2018-09-26 · YouTube · [Marp](../../../Documents/TimurShemsedinov/2018-09-26 — Обзор встроенного Node.js API (sOkjR-N6IAs).md)
  • Тезисы: http — и сервер, и клиент; для HTTPS отдельный модуль https; net для TCP/UDP уровня