Почему выбор Node.js-фреймворков снова вызывает раздражение — даже Express уже не выглядит идеальным
Разработчики всё чаще спорят, какой инструмент стоит своих нервов, а какой только усложняет жизнь — от Express и Koa до Fastify и Hono
Основные идеи
Мнение автора
В современном Node.js важно трезво оценивать масштаб и требования проекта. Минималистичные решения ускоряют старт, но крупные системы нуждаются в чёткой архитектуре. Стоит выбирать фреймворк не по моде, а по тому, насколько он снижает сложность и облегчает поддержку на длительной дистанции.
Node.js давно превратился в популярную среду для серверной разработки: он позволяет писать привычным JavaScript без браузера и дарит доступ к огромной экосистеме. Именно эта экосистема и формирует главное преимущество платформы, хотя иногда она превращает выбор подходящего инструмента в тот ещё квест.
В этой части мы пройдёмся по самым известным минималистичным решениям — Express, Koa, Fastify, Hono и Nitro. Эти фреймворки часто выглядят похожими, но у каждого свои характерные черты.
Минималистичные фреймворки
Каждый разработчик рано или поздно сталкивается с ситуацией, когда нужен минимум возможностей, но максимум гибкости. Именно на этом и строятся лёгкие фреймворки: они дают базовый функционал, а всё остальное пользователь наращивает сам через плагины.
Express.js
Express давно закрепился как самый распространённый пакет из npm, и это не удивляет. Он даёт простой доступ к маршрутам и обработке запросов, а при необходимости можно подключить массу middleware. Такой набор идеально подходит для проектов, где основная задача — просто организовать HTTP-эндпоинты.
Вот пример простого маршрута Express, который возвращает породу собаки по ID. Он демонстрирует традиционную структуру: путь, затем функция обработки запроса и ответа.
import express from 'express';
const app = express();
const port = 3000;
// In-memory array of dog breeds
const dogBreeds = [
"Shih Tzu",
"Great Pyrenees",
"Tibetan Mastiff",
"Australian Shepherd"
];
app.get('/dogs/:id', (req, res) => {
const id = parseInt(req.params.id, 10);
if (id >= 0 && id < dogBreeds.length) {
res.status(200).json({ breed: dogBreeds[id] });
} else {
res.status(404).json({ error: 'Dog breed not found' });
}
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
Многих удивляет отсутствие файловой маршрутизации, которая есть в более современных фреймворках. Но Express компенсирует это богатой коллекцией middleware.
Koa
Koa создавался как попытка переосмыслить Express. Разработчики хотели избавить архитектуру от старых решений и сделать код чище. В Koa вся логика строится вокруг async/await, а контекст объединён в один объект, поэтому API выглядит аккуратнее.
Пример того же маршрута в Koa:
router.get('/dogs/:id', (ctx) => {
const id = parseInt(ctx.params.id, 10);
if (id >= 0 && id < dogBreeds.length) {
ctx.status = 200;
ctx.body = { breed: dogBreeds[id] };
} else {
ctx.status = 404;
ctx.body = { error: 'Dog breed not found' };
}
});
И пример простого middleware-логгера:
const logger = async (ctx, next) => {
await next();
console.log(`${ctx.method} ${ctx.url} - ${ctx.status}`);
};
app.use(logger);
По ощущениям, сервер в Koa получается чище и более контролируемым.
Fastify
Fastify выделяется тем, что предлагает явно описывать схемы API. Это помогает документировать приложение и заранее отслеживать ошибки. Если схема не нужна, можно спокойно писать маршруты без неё, и тогда Fastify будет выглядеть почти как Express — только быстрее.
const schema = {
params: {
type: 'object',
properties: {
id: { type: 'integer' }
}
},
response: {
200: {
type: 'object',
properties: {
breed: { type: 'string' }
}
},
404: {
type: 'object',
properties: {
error: { type: 'string' }
}
}
}
};
fastify.get('/dogs/:id', { schema }, (request, reply) => {
const id = request.params.id;
if (id >= 0 && id < dogBreeds.length) {
reply.code(200).send({ breed: dogBreeds[id] });
} else {
reply.code(404).send({ error: 'Dog breed not found' });
}
});
Fastify подходит тем, кому важна предсказуемость API и скорость.
Hono
Hono делает ставку на минимализм. В нём легко создать сервер за пару строк, а маршруты читаются максимально компактно.
const app = new Hono();
app.get('/', (c) => c.text('Hello, DGL!'));
Пример маршрута с выбором породы по ID:
app.get('/dogs/:id', (c) => {
const id = parseInt(c.req.param('id'), 10);
if (id >= 0 && id < dogBreeds.length) {
return c.json({ breed: dogBreeds[id] });
} else {
c.status(404);
return c.json({ error: 'Dog breed not found' });
}
});
По духу Hono ближе к Koa, но синтаксис ещё проще.
Nitro
Nitro — серверный движок, который стоит за многими фулл-стек-фреймворками. Он даёт удобную файловую маршрутизацию и средства для запуска в облачных окружениях. Можно сказать, что Nitro занимает «золотую середину» между минимализмом и полноценной архитектурой.
Пример обработчика маршрута:
export default defineEventHandler((event) => {
const { id } = getRouterParams(event);
const parsedId = parseInt(id, 10);
if (parsedId >= 0 && parsedId < dogBreeds.length) {
return { breed: dogBreeds[parsedId] };
} else {
setResponseStatus(event, 404);
return { error: 'Dog breed not found' };
}
});
На практике Nitro выбирают, когда нужно быстрее собирать серверную часть, не жертвуя архитектурной ясностью.
В мире Node.js есть целый пласт инструментов, которые предлагают не минималистичный подход, а полноценную архитектуру «всё в одном». Это удобно на долгой дистанции, хотя иногда такие решения перегружают проект там, где можно было бы обойтись куда проще.
Фреймворки с полной комплектацией
Когда команда хочет строгую структуру и понятное масштабирование, на сцену выходят тяжёлые фреймворки. Они добавляют контроллеры, сервисы, модули, ORM и другие удобства, которые помогают держать проект в порядке.
Nest.js
Nest — один из самых популярных «архитектурных» инструментов. Он построен на TypeScript и во многом напоминает Angular. Здесь есть внедрение зависимостей, аннотированные контроллеры и строгая структура проекта.
Пример сервиса и контроллера:
// The provider:
import { Injectable, NotFoundException } from '@nestjs/common';
@Injectable()
export class DogsService {
private readonly dogBreeds = [
"Shih Tzu",
"Great Pyrenees",
"Tibetan Mastiff",
"Australian Shepherd"
];
findOne(id: number) {
if (id >= 0 && id < this.dogBreeds.length) {
return { breed: this.dogBreeds[id] };
}
throw new NotFoundException('Dog breed not found');
}
}
// The controller
import { Controller, Get, Param, ParseIntPipe } from '@nestjs/common';
import { DogsService } from './dogs.service';
@Controller('dogs')
export class DogsController {
constructor(private readonly dogsService: DogsService) {}
@Get(':id')
findOneDog(@Param('id', ParseIntPipe) id: number) {
return this.dogsService.findOne(id);
}
}
Такой стиль ценят команды, которым важно, чтобы проект выглядел одинаково у всех участников.
Adonis.js
Adonis тоже следует MVC-модели и добавляет ORM, удобные валидаторы и чёткое разделение слоёв. Маршруты выглядят просто, а логика контроллеров напоминает классические серверные фреймворки.
Route.get('/dogs/:id', [DogsController, 'show'])
Пример контроллера:
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class DogsController {
public async show({ params, response }: HttpContextContract) {
const id = Number(params.id);
if (!isNaN(id) && id >= 0 && id < this.dogBreeds.length) {
return response.ok({ breed: this.dogBreeds[id] });
} else {
return response.notFound({ error: 'Dog breed not found' });
}
}
}
В реальном проекте к этому добавляется слой моделей, отвечающий за доступ к данным.
Sails
Sails — один из старейших комплексных фреймворков для Node. В нём есть ORM Waterline, генератор API и встроенная поддержка WebSockets. Он рассчитан на тех, кому нужен максимально готовый сервер «из коробки».
Пример примитивной модели:
/**
* Dog.js
*
* @description :: A model definition represents a database table/collection.
*/
module.exports = {
attributes: {
breed: { type: 'string', required: true },
},
};
После запуска Sails сам создаёт маршруты и подключает хранилище данных. При необходимости разработчик может переопределить любую часть поведения.
Полноценные фулл-стек-фреймворки
Когда разработчику нужен и бэкенд, и фронтенд в одном проекте, на сцене появляются мета-фреймворки. Они позволяют запускать весь стек единым решением.
Next.js
Next, построенный на React, стал фактическим стандартом фулл-стек-разработки. Он объединяет API-эндпоинты и интерфейс в одном проекте и использует файловую маршрутизацию.
Пример API:
export default function handler(req, res) {
const { id } = req.query;
const parsedId = parseInt(id, 10);
if (parsedId >= 0 && parsedId < dogBreeds.length) {
res.status(200).json({ breed: dogBreeds[parsedId] });
} else {
res.status(404).json({ error: 'Dog breed not found' });
}
}
UI-страница:
function DogPage({ dog }) {
if (!dog) {
return <h1>Dog Breed Not Found</h1>;
}
return (
<div>
<h1>Dog Breed Profile</h1>
<p>Breed Name: <strong>{dog.breed}</strong></p>
</div>
);
}
export async function getServerSideProps(context) {
const { id } = context.params;
const res = await fetch(`http://localhost:3000/api/dogs/${id}`);
const dog = res.ok ? await res.json() : null;
return { props: { dog } };
}
export default DogPage;
Nuxt.js
Nuxt делает то же самое для Vue — серверные маршруты, готовые методы получения данных и файловую структуру.
Пример API-обработчика:
export default defineEventHandler((event) => {
const id = getRouterParam(event, 'id');
const parsedId = parseInt(id, 10);
if (parsedId >= 0 && parsedId < dogBreeds.length) {
return { breed: dogBreeds[parsedId] };
} else {
setResponseStatus(event, 404);
return { error: 'Dog breed not found' };
}
});
UI-страница:
<template>
<div>
<div v-if="pending">Loading...</div>
<div v-else-if="error">
<h1>{{ error.data.error }}</h1>
</div>
<div v-else>
<h1>Dog Breed Profile</h1>
<p>Breed Name: <strong>{{ dog.breed }}</strong></p>
</div>
</div>
</template>
<script setup>
const route = useRoute();
const { id } = route.params;
const { data: dog, pending, error } = await useFetch(`/api/dogs/${id}`);
</script>
SvelteKit
SvelteKit опирается на ту же идею, но использует Svelte — лёгкую реактивную библиотеку.
Пример API:
import { json, error } from '@sveltejs/kit';
export function GET({ params }) {
const id = parseInt(params.id, 10);
if (id >= 0 && id < dogBreeds.length) {
return json({ breed: dogBreeds[id] });
}
throw error(404, 'Dog breed not found');
}
Загрузка данных:
export async function load({ params, fetch }) {
const response = await fetch(`/api/dogs/${params.id}`);
if (response.ok) {
const dog = await response.json();
return { dog };
}
throw error(response.status, 'Dog breed not found');
}
UI-файл:
<script>
export let data;
</script>
<div>
<h1>Dog Breed Profile</h1>
<p>Breed Name: <strong>{data.dog.breed}</strong></p>
</div>
Заключение
Node.js давно перестал быть миром, где любой проект автоматически строится вокруг Express. Сейчас важно подобрать инструмент под конкретную задачу.
Если нужен быстрый микросервис или минимальный API, разработчики обращаются к Fastify или Hono.
Если команда работает над крупной системой, Nest и Adonis дают структуру, которая экономит силы в долгосрочной перспективе.
А для контентных веб-проектов самыми удобными остаются Next, Nuxt и SvelteKit.
Даже альтернативные рантаймы вроде Deno и Bun всё активнее вмешиваются в гонку, предлагая свою архитектуру и собственные фреймворки.
Современная разработка требует обдуманного выбора: каждая задача раскрывается лучше с подходящим инструментом.
Даже самый продвинутый фреймворк может стать обузой, если его воткнуть не туда.
И наоборот — грамотно подобранный инструмент часто превращает сложный проект в аккуратную систему.
Ваш любимый Python для ИИ — большая ошибка. Пора переходить на JavaScript











