Модуль 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' })
});
Частые ошибки
- Забывают
res.end()— клиент ждёт ответ бесконечно (hang), соединение не закрывается - Двойной ответ —
res.end()вызван дважды (например, в callback и после него) →ERR_HTTP_HEADERS_SENT - Не обрабатывают POST-тело —
req.bodyне существует в чистомhttp, данные приходят через Stream - writeHead после write — заголовки нужно отправить ДО тела ответа
- Игнорируют CORS — браузер блокирует запросы с другого домена без CORS-заголовков
- Синхронное чтение файлов —
readFileSyncв обработчике запроса блокирует весь сервер
Практика
- Создать HTTP-сервер, отвечающий JSON на GET
/api/timeс текущим временем - Реализовать POST
/api/echo, который возвращает полученные данные обратно - Написать простой static-файл-сервер с корректными MIME-типами
- Добавить роутинг: GET, POST, PUT, DELETE для ресурса
/api/todos - Реализовать graceful shutdown через SIGTERM
Связанные темы
- Stream API -- потоки — req и res как потоки
- fs — чтение статических файлов
- path — построение путей к файлам
- Что такое Express — фреймворк поверх http
Ресурсы
🎓 Источник: 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 уровня