spinny:~/writing $ vim hono-framework-guide.md
1~2Годами выбор JavaScript веб-фреймворка означал принятие компромисса: Express универсален, но медленный и привязан к Node, Fastify быстр, но только для Node, Next.js полнофункционален, но тяжёлый. С появлением edge-рантаймов — Cloudflare Workers, Deno Deploy, Bun, Vercel Edge — эти фреймворки показали свои пределы: несовместимые зависимости, огромные бандлы, API, привязанные к `req`/`res` Node.3~4[Hono](https://hono.dev) (с японского "пламя" 🔥) — современный ответ. Фреймворк весом менее 14KB, полностью построенный на Web Standards (`Request`, `Response`, `fetch`), работающий везде, где есть JavaScript-рантайм. Один и тот же код деплоится на Cloudflare Workers, Bun, Deno, Node.js, Vercel, Netlify и AWS Lambda — без изменений.5~6## Почему Hono7~8Hono делает три вещи лучше всех:9~101. **Производительность.** `RegExpRouter` компилирует все паттерны маршрутов в одно регулярное выражение, избегая линейных циклов традиционных маршрутизаторов. Бенчмарки превышают 400 000 ops/s, ставя Hono в число самых быстрых маршрутизаторов JavaScript-экосистемы.112. **Портативность.** Web Standards означает ноль зависимостей от Node. Один и тот же `app.fetch` экспортируется по умолчанию в Cloudflare Worker, передаётся в `Bun.serve`, монтируется в Deno-сервер или адаптируется через `@hono/node-server`.123. **TypeScript-first DX.** Параметры пути типизированы как литералы, типобезопасный RPC-клиент end-to-end, валидаторы, выводящие типы входа и выхода. Автодополнение почти телепатическое.13~14```mermaid15graph LR16 Client[Client] -->|Request| App[app.fetch]17 App --> MW1[Middleware 1]18 MW1 --> MW2[Middleware 2]19 MW2 --> Router[RegExpRouter]20 Router --> Handler[Route Handler]21 Handler --> Context[c.json / c.text]22 Context -->|Response| Client23 App -.->|deploy| CF[Cloudflare Workers]24 App -.->|deploy| Bun[Bun]25 App -.->|deploy| Deno[Deno]26 App -.->|deploy| Node[Node.js]27 App -.->|deploy| Vercel[Vercel]28```29~30## Начало работы31~32Самый быстрый путь — официальный стартер, который скаффолдит проект для выбранного рантайма.33~34```bash35npm create hono@latest my-api36cd my-api37npm install38npm run dev39```40~41Стартер спрашивает, какой шаблон использовать: `cloudflare-workers`, `bun`, `deno`, `nodejs`, `vercel`, `aws-lambda`, `nextjs` и другие. Для быстрой пробы можно начать и с одного файла:42~43```typescript44// src/index.ts45import { Hono } from 'hono'46~47const app = new Hono()48~49app.get('/', (c) => c.text('Hello Hono!'))50~51export default app52```53~54На Cloudflare Workers этого достаточно. На Bun: `Bun.serve({ fetch: app.fetch, port: 3000 })`. На Node: `serve({ fetch: app.fetch })` из `@hono/node-server`. Поверхность идентична.55~56## Маршрутизация57~58Маршруты объявляются методами HTTP-глаголов и поддерживают параметры, wildcard и regex.59~60```typescript61import { Hono } from 'hono'62~63const app = new Hono()64~65app.get('/', (c) => c.text('Home'))66app.get('/posts/:id', (c) => {67 const id = c.req.param('id') // string, типизировано68 return c.json({ id })69})70app.get('/posts/:id/comments/:commentId', (c) => {71 const { id, commentId } = c.req.param()72 return c.json({ id, commentId })73})74app.get('/files/*', (c) => c.text('Wildcard'))75app.post('/posts', async (c) => {76 const body = await c.req.json()77 return c.json({ created: body }, 201)78})79```80~81Параметры выводятся как литеральные типы: TypeScript знает, что `c.req.param('id')` возвращает `string` только если вы объявили `:id` в паттерне. Опечатка — ошибка времени компиляции.82~83### Группировка маршрутов84~85`app.route()` позволяет компоновать суб-приложения как модули, каждое со своим префиксом.86~87```typescript88// routes/posts.ts89import { Hono } from 'hono'90~91const posts = new Hono()92posts.get('/', (c) => c.json({ posts: [] }))93posts.get('/:id', (c) => c.json({ id: c.req.param('id') }))94export default posts95~96// src/index.ts97import { Hono } from 'hono'98import posts from './routes/posts'99~100const app = new Hono()101app.route('/posts', posts)102```103~104Вложенные маршруты наследуют base path и тип корневого приложения, поэтому RPC-клиент видит всю структуру.105~106## Объект Context107~108Каждый обработчик получает `c` — Context для текущего запроса. Это единственный API, который нужно изучить, чтобы читать вход и производить выход.109~110```typescript111app.post('/echo', async (c) => {112 // Чтение113 const userAgent = c.req.header('User-Agent')114 const page = c.req.query('page')115 const body = await c.req.json()116 const env = c.env // bindings (KV, D1, секреты на Cloudflare)117~118 // Переменные, разделяемые между middleware и обработчиком119 c.set('requestId', crypto.randomUUID())120 const id = c.get('requestId')121~122 // Ответ123 c.header('X-Request-Id', id)124 c.status(200)125 return c.json({ userAgent, page, body, id })126})127```128~129Основные методы ответа: `c.text`, `c.json`, `c.html`, `c.body` (raw), `c.redirect`. Все принимают код состояния вторым аргументом.130~131## Middleware: луковичная модель132~133Middleware — функции `(c, next) => ...`, которые могут выполнять код до и после обработчика. Скомпонованные, они образуют классическую луковичную модель: первый зарегистрированный middleware первым стартует и последним заканчивает.134~135```typescript136import { Hono } from 'hono'137import { logger } from 'hono/logger'138import { cors } from 'hono/cors'139import { secureHeaders } from 'hono/secure-headers'140~141const app = new Hono()142~143app.use('*', logger())144app.use('*', secureHeaders())145app.use('/api/*', cors({ origin: 'https://spinny.dev' }))146~147app.use('*', async (c, next) => {148 const start = performance.now()149 await next()150 c.header('X-Response-Time', `${performance.now() - start}ms`)151})152```153~154`await next()` передаёт управление следующему middleware. Всё, что после, выполняется в фазе ответа после того, как обработчик произвёл результат. Возврат `Response` до `next()` короткозамыкает цепочку.155~156### Встроенный middleware157~158| Middleware | Назначение |159|-----------|---------|160| `logger` | Структурированные логи метода, пути, статуса, длительности |161| `cors` | CORS, настраиваемый по origin, методам, заголовкам |162| `csrf` | Защита CSRF на основе origin |163| `secureHeaders` | Устанавливает CSP, HSTS, X-Frame-Options |164| `bearerAuth` / `basicAuth` | Готовая Bearer/Basic-аутентификация |165| `jwt` | Проверка/подпись JWT через `jose` |166| `etag` | Генерирует ETag и обрабатывает 304 |167| `cache` | Кэш через Web Cache API |168| `compress` | gzip/deflate ответа |169| `bodyLimit` | Отклоняет тела выше порога |170| `timing` | Заголовок Server-Timing для профилирования |171~172### Типобезопасный custom middleware173~174```typescript175import { createMiddleware } from 'hono/factory'176~177type AuthVars = { userId: string; role: 'user' | 'admin' }178~179export const requireAuth = createMiddleware<{ Variables: AuthVars }>(180 async (c, next) => {181 const token = c.req.header('Authorization')?.replace('Bearer ', '')182 if (!token) return c.json({ error: 'Unauthorized' }, 401)183~184 const payload = await verifyJwt(token)185 c.set('userId', payload.sub)186 c.set('role', payload.role)187 await next()188 }189)190~191app.get('/me', requireAuth, (c) => {192 const userId = c.var.userId // string, типизировано193 return c.json({ userId })194})195```196~197## Валидация с Zod198~199```typescript200import { Hono } from 'hono'201import { zValidator } from '@hono/zod-validator'202import { z } from 'zod'203~204const createPost = z.object({205 title: z.string().min(1).max(200),206 body: z.string().min(1),207 tags: z.array(z.string()).default([]),208})209~210app.post(211 '/posts',212 zValidator('json', createPost),213 (c) => {214 const data = c.req.valid('json')215 return c.json({ ok: true, post: data }, 201)216 }217)218```219~220Если тело не проходит валидацию, Hono отвечает 400 с ошибкой Zod ещё до вызова обработчика. Можно валидировать также `query`, `param`, `header`, `cookie` и `form`.221~222## RPC: типобезопасный клиент end-to-end223~224```typescript225// server.ts226import { Hono } from 'hono'227import { zValidator } from '@hono/zod-validator'228import { z } from 'zod'229~230const app = new Hono()231 .get('/posts/:id', (c) =>232 c.json({ id: c.req.param('id'), title: 'Hello' })233 )234 .post(235 '/posts',236 zValidator('json', z.object({ title: z.string(), body: z.string() })),237 (c) => c.json({ ok: true }, 201)238 )239~240export type AppType = typeof app241export default app242```243~244```typescript245// client.ts246import { hc } from 'hono/client'247import type { AppType } from './server'248~249const client = hc<AppType>('https://api.spinny.dev')250~251const res = await client.posts[':id'].$get({ param: { id: '42' } })252if (res.ok) {253 const data = await res.json()254 console.log(data.title)255}256~257const created = await client.posts.$post({258 json: { title: 'Привет', body: 'Hono — это пламя' },259})260```261~262Переименуйте маршрут на сервере, и TypeScript клиента сразу же сломается в CI. То же преимущество, что у tRPC, но поверх стандартного HTTP, без специфичного middleware и с крошечным бандлом.263~264### Дискриминация по статус-коду265~266```typescript267.get('/posts/:id', (c) => {268 const post = findPost(c.req.param('id'))269 if (!post) return c.json({ error: 'not found' }, 404)270 return c.json({ post }, 200)271})272```273~274```typescript275const res = await client.posts[':id'].$get({ param: { id } })276if (res.status === 404) {277 const { error } = await res.json() // { error: string }278}279if (res.status === 200) {280 const { post } = await res.json() // { post: Post }281}282```283~284## Маршрутизаторы и производительность285~286| Маршрутизатор | Сильные стороны | Когда использовать |287|--------|-----------|-------------|288| `RegExpRouter` | Максимальная скорость, скомпилированный regex | По умолчанию для большинства API |289| `TrieRouter` | Поддерживает любые паттерны | Сложные паттерны, не покрытые RegExp |290| `SmartRouter` | Выбирает лучший автоматически | Рекомендуемый по умолчанию |291| `LinearRouter` | Сверхбыстрая регистрация | One-shot воркеры, критичный cold start |292| `PatternRouter` | Минимальный бандл (<15KB) | Жёсткие ограничения по размеру |293~294```typescript295import { Hono } from 'hono'296import { LinearRouter } from 'hono/router/linear-router'297~298const app = new Hono({ router: new LinearRouter() })299```300~301## Мульти-рантайм деплой302~303### Cloudflare Workers304~305```typescript306import { Hono } from 'hono'307~308type Bindings = { MY_KV: KVNamespace; DB: D1Database }309const app = new Hono<{ Bindings: Bindings }>()310~311app.get('/cache/:key', async (c) => {312 const value = await c.env.MY_KV.get(c.req.param('key'))313 return c.json({ value })314})315~316export default app317```318~319Деплой: `npx wrangler deploy`. KV/D1/R2/Queues bindings нативны.320~321### Bun322~323```typescript324import { Hono } from 'hono'325const app = new Hono()326app.get('/', (c) => c.text('Bun + Hono'))327~328Bun.serve({ fetch: app.fetch, port: 3000 })329```330~331### Node.js332~333```typescript334import { serve } from '@hono/node-server'335import { Hono } from 'hono'336~337const app = new Hono()338app.get('/', (c) => c.text('Node + Hono'))339~340serve({ fetch: app.fetch, port: 3000 })341```342~343### Deno344~345```typescript346import { Hono } from 'jsr:@hono/hono'347const app = new Hono()348app.get('/', (c) => c.text('Deno + Hono'))349Deno.serve(app.fetch)350```351~352### Vercel353~354```typescript355// api/[[...route]].ts356import { Hono } from 'hono'357import { handle } from 'hono/vercel'358~359const app = new Hono().basePath('/api')360app.get('/hello', (c) => c.json({ msg: 'Hello from Vercel' }))361~362export const GET = handle(app)363export const POST = handle(app)364```365~366Тот же бизнес-код, та же маршрутизация, тот же middleware. Меняется только адаптер.367~368## Реальный пример: REST API с auth и DB369~370```typescript371import { Hono } from 'hono'372import { jwt } from 'hono/jwt'373import { logger } from 'hono/logger'374import { cors } from 'hono/cors'375import { zValidator } from '@hono/zod-validator'376import { z } from 'zod'377~378type Bindings = { DB: D1Database; JWT_SECRET: string }379type Variables = { jwtPayload: { sub: string } }380~381const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()382~383app.use('*', logger())384app.use('/api/*', cors({ origin: 'https://spinny.dev', credentials: true }))385~386const auth = (c: any, next: any) =>387 jwt({ secret: c.env.JWT_SECRET })(c, next)388~389const api = app.basePath('/api')390~391api.get('/posts', async (c) => {392 const { results } = await c.env.DB393 .prepare('SELECT id, title, created_at FROM posts ORDER BY created_at DESC LIMIT 50')394 .all()395 return c.json({ posts: results })396})397~398api.post(399 '/posts',400 auth,401 zValidator('json', z.object({402 title: z.string().min(1).max(200),403 body: z.string().min(1),404 })),405 async (c) => {406 const { title, body } = c.req.valid('json')407 const userId = c.var.jwtPayload.sub408 const result = await c.env.DB409 .prepare('INSERT INTO posts (title, body, author_id) VALUES (?, ?, ?) RETURNING id')410 .bind(title, body, userId)411 .first<{ id: number }>()412 return c.json({ id: result?.id }, 201)413 }414)415~416api.onError((err, c) => {417 console.error(err)418 return c.json({ error: 'Internal error' }, 500)419})420~421export type AppType = typeof api422export default app423```424~425React-клиент потребляет с полной типобезопасностью:426~427```typescript428import { hc } from 'hono/client'429import type { AppType } from '../api/src/index'430~431const api = hc<AppType>(import.meta.env.VITE_API_URL)432~433const res = await api.posts.$post({434 json: { title: 'Hono в 2026', body: '...' },435}, {436 headers: { Authorization: `Bearer ${token}` },437})438```439~440## Тестирование441~442```typescript443import { describe, it, expect } from 'vitest'444import app from '../src/index'445~446describe('GET /api/posts', () => {447 it('возвращает список постов', async () => {448 const res = await app.request('/api/posts')449 expect(res.status).toBe(200)450 const body = await res.json()451 expect(body.posts).toBeInstanceOf(Array)452 })453})454```455~456`app.request()` позволяет тестировать маршруты без поднятия HTTP-сервера.457~458## Лучшие практики459~460### 1. Цепочка определений маршрутов461~462```typescript463const app = new Hono()464 .get('/posts', handler1)465 .post('/posts', handler2)466 .get('/posts/:id', handler3)467```468~469Цепочка поддерживает тип `app` актуальным с каждым маршрутом — критично для RPC-клиента.470~471### 2. Экспортируй тип, не реализацию472~473Клиент должен импортировать `AppType`, а не приложение. `import type` гарантирует, что фронтенд-сборка не включит код бэкенда.474~475### 3. Один маршрутизатор на домен476~477Под-приложение для `posts`, для `users`, для `webhooks`. Компонуй через `app.route()`.478~479### 4. Валидация на границе, всегда480~481Каждый внешний вход (body, query, header) должен пройти через `zValidator`.482~483### 5. Опирайся на bindings, а не глобальные клиенты484~485На Cloudflare доступ к KV/D1/R2 через `c.env`.486~487### 6. Замеряй до оптимизации маршрутизатора488~489Дефолтный `SmartRouter` подходит для 95% случаев.490~491## Заключение492~493Hono стал в 2026 году де-факто стандартом для построения edge-готовых API на TypeScript. Сочетание Web Standards, производительности, типобезопасности и портативности решает именно те проблемы, которые сдерживали традиционные фреймворки.494~495> **Чек-лист для старта:**496>497> - [x] `npm create hono@latest` и выберите шаблон рантайма498> - [x] Определите маршруты цепочкой (`.get(...).post(...)`)499> - [x] Добавьте `logger`, `cors`, `secureHeaders` как глобальный middleware500> - [x] Валидируйте каждый вход с `@hono/zod-validator`501> - [x] Экспортируйте `AppType` и используйте API через типобезопасный `hc`-клиент502> - [x] Пишите тесты с `app.request()` — без HTTP-сервера503> - [x] Деплойте через `wrangler deploy` (CF), `vercel deploy` или бандлер вашего рантайма504~
NORMAL · hono-framework-guide.md [readonly]504 lines · :q to close