Почти любой бэкенд на Node это HTTP-сервер. Под капотом всегда модуль node:http, сверху фреймворк: минималистичный Express, быстрый Fastify или структурный NestJS.
Голый node:http
import http from 'node:http';
const server = http.createServer(async (req, res) => {
if (req.method === 'POST' && req.url === '/echo') {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
const body = JSON.parse(Buffer.concat(chunks).toString());
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(body));
return;
}
res.writeHead(404).end();
});
server.listen(3000);
Что видно из примера:
reqэто Readable-поток: тело запроса приходит кусками, его надо прочитать.resэто Writable-поток: сначала статус и заголовки, потом тело, в концеend().- Роутинга, парсинга JSON и обработки ошибок нет: это и добавляют фреймворки.
Если попытаться выставить заголовок после того, как ответ начал отправляться, будет ошибка ERR_HTTP_HEADERS_SENT. Типичная причина: два res.send в одном обработчике, например забытый return после ответа об ошибке.
Express: цепочка middleware
Express это список функций (req, res, next), которые выполняются по порядку регистрации.
import express from 'express';
const app = express();
app.use(express.json()); // парсит JSON в req.body
app.use((req, res, next) => { // логирование
const start = Date.now();
res.on('finish', () => console.log(req.method, req.url, Date.now() - start));
next();
});
app.get('/users/:id', async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) return res.status(404).json({ error: 'not found' });
res.json(user);
});
app.use((err, req, res, next) => { // обработчик ошибок: 4 аргумента
res.status(500).json({ error: 'internal' });
});
Правила:
- middleware либо отвечает, либо вызывает
next(). Если не сделать ни того, ни другого, запрос повиснет. next(err)пропускает обычные middleware и идёт к обработчику ошибок. Express узнаёт его по четырём параметрам.- Без
express.json()req.bodyбудетundefined. - Express 5 (актуальная версия) сам ловит отклонённые промисы из async-обработчиков и передаёт их в
next(err). В Express 4 такая ошибка улетала мимо, и нужна была обёртка.
NestJS: структура и DI
NestJS строит приложение из модулей, контроллеров и провайдеров и использует внедрение зависимостей. По умолчанию работает поверх Express, можно переключить на Fastify.
@Injectable()
export class UsersService {
constructor(private readonly prisma: PrismaService) {}
findOne(id: string) { return this.prisma.user.findUnique({ where: { id } }); }
}
@Controller('users')
export class UsersController {
constructor(private readonly users: UsersService) {}
@Get(':id')
@UseGuards(JwtAuthGuard)
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.users.findOne(id);
}
}
Контейнер DI сам создаёт UsersService и передаёт его в контроллер. По умолчанию провайдеры синглтоны на всё приложение.
Путь запроса через NestJS:
- Middleware (как в Express).
- Guards: можно ли выполнять запрос (авторизация, роли).
- Interceptors до обработчика (логирование, кеш, таймаут).
- Pipes: валидация и преобразование параметров (
ValidationPipe,ParseIntPipe). - Обработчик контроллера.
- Interceptors после: могут преобразовать ответ.
- Exception filters: ловят исключения и формируют ответ об ошибке.
Express, Fastify или NestJS
- Express: минимум, огромная экосистема, свобода и ответственность за структуру.
- Fastify: заметно быстрее за счёт схем и быстрой сериализации, встроенная валидация по JSON Schema, плагины с инкапсуляцией.
- NestJS: архитектура из коробки (модули, DI, декораторы), удобен для больших команд, работает поверх Express или Fastify.
Частые ошибки
- Забытый
returnпосле ответа:ERR_HTTP_HEADERS_SENT. - Обработчик ошибок с тремя параметрами: Express не считает его обработчиком ошибок.
- Middleware, зарегистрированный после роутов, которых он должен касаться.
- Тяжёлая синхронная работа в обработчике: блокирует все запросы.
- В NestJS хранить данные запроса в поле синглтон-сервиса: их увидят параллельные запросы.
Что спросят на собесе
- Что такое middleware и как работает
next. - Как в Express обработать ошибку из async-обработчика.
- В каком порядке выполняются guards, interceptors, pipes и filters в NestJS.
- Что такое DI и зачем он во фреймворке.
- Чем Fastify отличается от Express.