← к блогу

// бэкенд

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

Обложка статьи: «Hono вместо Express»

Я пишу контент-сервис для блога. Маленький 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, привычный выбор внезапно оказывается не тем.

Есть похожая задача?

Расскажите — вернусь с оценкой в тот же день.

Обсудить проект

// читать дальше

Кадр из клипа про то, почему ИИ-агенты рисуют серые интерфейсы
0:36
// видео

Почему ИИ-агенты рисуют одинаково серые интерфейсы

Обложка статьи: «Claude Design в проде»
// дизайн6 мин

Как использовать Claude Design в проде

Я разработчик, дизайнер из меня никакой. Но сайт, который ты читаешь, выглядит как надо — дизайн я собрал с Claude, руками не рисовал. Показываю весь путь и артефакты, которые реально пошли в работу.

Обложка статьи: «1 блог, 2 сайта, 1 сервис»
// архитектура8 мин

Два сайта — один блог: как я не уронил SEO, сливая контент в один сервис

Слил блоги двух сайтов в один сервис на Hono и Turso. База оказалась простой частью, вся коварность — в доставке: контент должен приезжать на билде, потому что при фетче на клиенте превью-боты ловят пустую страницу.