// бэкенд
Express я выкинул через два вечера. Взял Hono — и бэкенд перестал сопротивляться

Я пишу контент-сервис для блога. Маленький API, который отдаёт посты на два моих сайта. Hono, Prisma, Turso.
Но начинал я не с Hono. Первый каркас набросал на Express, потому что так делают все и пальцы помнят. Через два вечера я его выкинул.
Не из любви к новому. Меня добили две конкретные вещи: типы, которые приходится дописывать руками, и тесты, которые требуют поднимать живой сервер ради одного GET. Hono обе проблемы снимает. И я хочу честно разобрать как — без хайпа и без презрения к Express.
Что меня добило в Express
Express писали в эпоху, когда TypeScript ещё не было. Типы к нему приклеили потом, отдельным пакетом @types/express — и это чувствуется на каждом запросе. req.params.site — это string или undefined? А req.body — это any. Буквально any. Ты валидируешь тело руками, а компилятор молча кивает на любую дичь.
Дальше бойлерплейт. Хочешь поймать ошибку из async-хендлера — оборачивай в try/catch или тащи express-async-errors, иначе промис-реджект утечёт мимо обработчика ошибок. А сам res мутабельный: вместо того чтобы вернуть ответ, ты собираешь его побочными эффектами. Забыл res.end или дважды дёрнул send — получай рантайм-сюрприз, который типы не ловят в принципе.
Что даёт Hono
Hono спроектирован вокруг TypeScript и веб-стандартов с нуля. Это меняет ощущение от кода сильнее, чем я ожидал. Главное — три вещи.
- Типы выводятся сами. Объявил /:site — и c.req.param('site') уже string, без ручных аннотаций.
- Ответ — это просто return, без мутаций. Хендлер возвращает c.json(data): под капотом обычный Web Response. Никаких res.end и двойных send.
- Middleware с настоящим async/await. Пишешь await next() — и оборачивать роуты в try/catch ради ошибок больше не нужно, цепочка прозрачная.
Под капотом всё построено на Web-стандартах: Request и Response — те самые, что в браузерном fetch. Звучит как косметика, но нет: ровно тот же код хендлера крутится на Node, Bun, Deno, в Cloudflare Workers — на любом рантайме, который понимает fetch. Мне нужен был Node, я взял @hono/node-server как тонкую обёртку. Но я знаю, что не заперт. Вот реальный кусок из сервиса — роут со срезом постов под конкретный сайт и middleware-логгер. Сайт типизирован, ответ просто возвращается.
const app = new Hono()
app.use('*', async (c, next) => {
const start = Date.now()
await next()
console.log(c.req.method, c.req.path, Date.now() - start + 'ms')
})
app.get('/v1/posts/:site', (c) => {
const site = c.req.param('site') // string, выведен из пути
const posts = selectForSite(site)
return c.json({ posts })
})Тесты без живого сервера
А вот это для меня оказалось решающим. У Hono есть app.request() — он прогоняет запрос через всё приложение как обычный Web Request и возвращает Response. Без сокетов, без портов, без supertest, без беготни с поднять-сервер-перед-тестами и убить-после. В Express роут обычно проверяют через supertest, который слушает реальный порт: медленнее, флакает на параллельных прогонах из-за занятых портов, лишний слой.
const res = await app.request('/v1/posts/main')
expect(res.status).toBe(200)
const { posts } = await res.json()
expect(posts.every((p) => p.audiences.includes('main'))).toBe(true)Мои тесты на vitest гоняют логику курации per-site против изолированной test.db за миллисекунды, и я держу их в watch на каждое сохранение. Когда тесты дешёвые — ты их пишешь. Хороший фреймворк — тот, про который ты забываешь: он не отнимает внимание на себя, а отдаёт его задаче.
Где Express всё ещё честно ок
Я не пришёл хоронить Express. Главный его аргумент — экосистема. Под него есть middleware буквально на всё: passport, multer, древние-но-рабочие интеграции, ответы на StackOverflow на любой твой затык в три часа ночи. Это пятнадцать лет накопленного знания, и оно реально экономит время.
- Легаси и команда: если сервис уже на Express и десяток людей его знают — переписывать ради типов это решение с отрицательным ROI.
- Узкие интеграции: нужен конкретный старый middleware без аналога под Web-стандарт — Express довезёт быстрее.
- Найм: разработчиков с Express в резюме просто больше, и онбординг короче.
Hono я выбрал потому, что сервис новый, маленький и мой: один автор, чистый TypeScript, никакого легаси-багажа. В этих условиях TS-first роутинг и app.request() окупаются с первого дня. Бэкенд для меня — продолжение того же фронтового мышления, никакой отдельной вселенной: Request на входе, Response на выходе, типы держат всю цепочку. Hono говорит на этом языке — Express заставляет переводить.
P.S. На очереди — база. Почему под прод я взял SQLite на Turso вместо Postgres: когда нагрузка read-heavy, привычный выбор внезапно оказывается не тем.
Есть похожая задача?
Расскажите — вернусь с оценкой в тот же день.


