Як працює модуль HTTP у Node.js?
Модуль http є вбудованим у Node.js і дозволяє створювати HTTP-сервери та виконувати вихідні HTTP-запити без жодних npm-залежностей.
Теорія
TL;DR
- Кожен HTTP-сервер у Node запускається в одному процесі, але обробляє багато з'єднань через event loop
reqєIncomingMessage(читабельний потік),resєServerResponse(записуваний потік)- Тіло запиту надходить не одразу: збираєш його з подій
dataвручну http.createServer()під капотом повертаєnet.Server. Express огортає ту саму функціюhttpsберемо для продакшну (TLS),http2коли потрібен multiplexing
Швидкий приклад
const http = require('http');
const server = http.createServer((req, res) => {
// req = IncomingMessage (читабельний потік)
// res = ServerResponse (записуваний потік)
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Hello' }));
});
server.listen(3000, () => console.log('Сервер на порту 3000'));Callback спрацьовує на кожний вхідний запит. res.end() відправляє відповідь і закриває з'єднання.
Як працює цикл запит-відповідь
Коли клієнт підключається, TCP-шар Node приймає з'єднання і починає парсити HTTP-повідомлення. Після того як заголовки розібрані, Node викликає твій callback з req і res. Тіло запиту в цей момент ще не прочитане: воно продовжує надходити через потік. Тому події data і end потрібно слухати вручну.
res.writeHead() записує рядок статусу і заголовки. res.end() відправляє тіло і закриває відповідь. Про відсутній res.end() найкраще дізнаєшся один раз, коли curl просто зависає і ти десять хвилин дивишся в консоль в пошуках проблеми.
Читання об'єкта запиту
const server = http.createServer((req, res) => {
console.log(req.method); // 'GET', 'POST', ...
console.log(req.url); // '/api/users?page=2'
console.log(req.headers); // { host: 'localhost:3000', ... }
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');
});req.url є сирим рядком. Для парсингу query-параметрів використовуй вбудований конструктор URL. Розбиття рядка ламається одразу, як тільки з'являється query string.
Збирання тіла POST-запиту
Тіло надходить частинами. Збираєш їх сам:
const server = http.createServer((req, res) => {
if (req.method === 'POST' && req.url === '/api/users') {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
const user = JSON.parse(body);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ created: user }));
});
}
});Тут немає ліміту розміру за замовчуванням. Клієнт може надсилати гігабайти. У продакшні завжди обмежуй розмір тіла. Express робить це автоматично через bodyParser.
Вихідні HTTP-запити
const http = require('http');
// GET
http.get('http://api.example.com/data', (res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => console.log(JSON.parse(data)));
});
// POST
const payload = JSON.stringify({ name: 'Alice' });
const req = http.request(
{
hostname: 'api.example.com',
port: 80,
path: '/users',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(payload),
},
},
(res) => {
let data = '';
res.on('data', (chunk) => (data += chunk));
res.on('end', () => console.log(JSON.parse(data)));
}
);
req.write(payload);
req.end();Callback у http.request() є скороченням для події response. Без req.end() запит не відправиться.
http vs https vs http2
| Модуль | Протокол | Коли використовувати |
|---|---|---|
http | HTTP/1.1 | Локальна розробка, внутрішні сервіси в приватній мережі |
https | HTTPS + TLS | Будь-який продакшн-сервер для реальних користувачів |
http2 | HTTP/2 | Коли потрібен multiplexing або server push |
https потребує TLS-сертифікати. http2 використовує інший API (http2.createSecureServer) і підтримує мультиплексовані потоки через одне TCP-з'єднання.
http vs Express
| Функція | Сировинний http | Express.js |
|---|---|---|
| Маршрутизація | Ручна if/else по req.url | app.get('/path', handler) |
| Парсинг тіла | Ручне збирання потоку | express.json() middleware |
| Middleware | Відсутнє | Підтримується з коробки |
| Статичні файли | Вручну | express.static() |
Express викликає http.createServer() всередині. Коли пишеш app.listen(3000), Express запускає http.createServer(app).listen(3000) за лаштунками. Ти завжди використовуєш http. Express просто дає кращий API зверху.
Типові помилки
1. Забутий res.end()
// Зламано - клієнт зависає нескінченно
http.createServer((req, res) => {
res.writeHead(200);
// res.end() відсутній
});Потік відповіді ніколи не закривається. Клієнт чекає до таймауту.
2. Запис заголовків після початку тіла
// Зламано
http.createServer((req, res) => {
res.write('some data');
res.writeHead(200); // Error: Cannot set headers after they are sent
});res.writeHead() має стояти перед будь-яким res.write().
3. Немає обробника помилок на req
// Тендітно - процес впаде при обриві з'єднання
req.on('data', (chunk) => { body += chunk; });
// Правильно
req.on('data', (chunk) => { body += chunk; });
req.on('error', (err) => {
res.writeHead(400);
res.end(err.message);
});Якщо клієнт обриває з'єднання посередині запиту, Node генерує подію error на req. Без обробника процес впаде.
4. Немає ліміту розміру тіла
let body = '';
req.on('data', (chunk) => {
body += chunk;
if (body.length > 1e6) { // ліміт 1MB
req.destroy();
res.writeHead(413);
res.end('Payload too large');
}
});5. Парсинг req.url через розбиття рядка
// Ламається при query string типу /users/42?format=json
const id = req.url.split('/')[2];
// Надійно
const url = new URL(req.url, `http://${req.headers.host}`);
const id = url.pathname.split('/')[2];Де зустрічається в продакшні
- Express і Fastify обидва викликають
http.createServer()під капотом - Next.js custom servers використовують його напряму
- Легковагові Node-мікросервіси без повноцінного фреймворку
http.get()іhttp.request()часто зустрічаються в старих кодових базах, написаних до того якfetchз'явився в Node 18
Питання на співбесіді
Q: Чому Express використовує http.createServer() замість власного TCP-шару?
A: Тому що http є вбудованим TCP-парсером і обробником HTTP-повідомлень у Node. Кожен Node HTTP-фреймворк проходить через нього. Express додає маршрутизацію, ланцюжки middleware і зручніші хелпери поверх того самого callback.
Q: Що таке IncomingMessage і чому він розширює readable stream?
A: IncomingMessage представляє вхідне HTTP-повідомлення. Він розширює stream.Readable, бо тіло може бути довільно великим. Читання через потік дозволяє не завантажувати весь payload у пам'ять одразу.
Q: Як Node обробляє паралельні запити, якщо він однопоточний?
A: Event loop обробляє I/O асинхронно. Поки один запит чекає відповіді від бази даних, loop підхоплює інші вхідні з'єднання. Потоки для I/O-bound задач не потрібні.
Q: Що станеться, якщо викликати res.end() двічі?
A: Node кине Error: write after end. Другий виклик намагається записати в уже закритий потік.
Q: Як захистити http-сервер від клієнтів що надсилають великі тіла?
A: Виставити ліміт розміру всередині обробника data і викликати req.destroy() при перевищенні порогу. Також варто виставити server.timeout, щоб автоматично закривати неактивні з'єднання.
Приклади
Простий JSON API
const http = require('http');
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
];
const server = http.createServer((req, res) => {
if (req.method === 'GET' && req.url === '/api/users') {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(users));
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Not found' }));
});
server.listen(3000);GET на /api/users повертає список. Все інше отримує 404. Той самий патерн, що використовують роути Express, тільки без шару абстракції.
Створення користувача через POST
const http = require('http');
const server = http.createServer((req, res) => {
if (req.method !== 'POST' || req.url !== '/api/users') {
res.writeHead(404);
res.end();
return;
}
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
if (body.length > 1e5) { // захист 100KB
req.destroy();
}
});
req.on('end', () => {
try {
const user = JSON.parse(body);
res.writeHead(201, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ id: Date.now(), ...user }));
} catch {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
req.on('error', () => {
res.writeHead(400);
res.end();
});
});
server.listen(3000);Повний продакшн-патерн: захист від великого тіла, обробка помилки парсингу JSON і слухач помилок на потоці запиту.
Проксування запиту до внутрішнього сервісу
const http = require('http');
const server = http.createServer((req, res) => {
const proxy = http.request(
{
hostname: 'internal-service',
port: 8080,
path: req.url,
method: req.method,
headers: req.headers,
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res); // стрімимо відповідь назад клієнту
}
);
req.pipe(proxy); // стрімимо тіло до внутрішнього сервісу
proxy.on('error', () => {
res.writeHead(502);
res.end('Bad gateway');
});
});
server.listen(3000);Pipe req напряму у proxy-запит дозволяє уникнути буферизації тіла в пам'яті. Відповідь повертається так само. Це концептуальна основа HTTP-реверс-проксі.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.