TypeScript с Hono: типобезопасный веб-фреймворк

30 октября 2026
Автор

Антон Ларичев

Что такое Hono

Hono (яп. «пламя») — ультралёгкий веб-фреймворк для TypeScript и JavaScript, изначально созданный для Cloudflare Workers. Сегодня он работает на любой среде выполнения: Node.js, Bun, Deno, Vercel Edge, AWS Lambda и других.

Главное преимущество Hono перед Express или Fastify — это сквозная типобезопасность. Типы параметров маршрута, тела запроса и ответа выводятся автоматически из схем валидации, без ручных аннотаций.

Курс по теме

Освоить TypeScript с нуля на курсе

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Установка и начало работы

Для Node.js-проекта:

npm init -y
npm install hono
npm install -D typescript @types/node tsx
npx tsc --init

Минимальный сервер:

import { Hono } from 'hono'
import { serve } from '@hono/node-server'

const app = new Hono()

app.get('/', (c) => {
  return c.text('Hello, Hono!')
})

serve({ fetch: app.fetch, port: 3000 })

Для Bun установка ещё проще — Bun встроен в среду выполнения Hono:

bun create hono my-app
cd my-app
bun run dev

Типизированный контекст

Центральный объект в Hono — Context (обычно именуется c). Он инкапсулирует запрос и ответ, предоставляя строго типизированный интерфейс.

import { Hono } from 'hono'

const app = new Hono()

app.get('/hello', (c) => {
  // c.req — типизированный запрос
  const name = c.req.query('name') // string | undefined
  
  // c.json, c.text, c.html — типизированные хелперы ответа
  return c.json({ message: `Hello, ${name ?? 'World'}` })
})

Параметры маршрута с типами

Hono автоматически выводит тип параметров из строки пути:

import { Hono } from 'hono'

const app = new Hono()

// TypeScript знает, что c.req.param('id') — string
app.get('/users/:id', (c) => {
  const id = c.req.param('id') // тип: string
  return c.json({ userId: id })
})

// Несколько параметров
app.get('/posts/:postId/comments/:commentId', (c) => {
  const postId = c.req.param('postId')     // string
  const commentId = c.req.param('commentId') // string
  return c.json({ postId, commentId })
})

Переменные окружения и типизация приложения

Hono поддерживает дженерики для типизации переменных окружения, переменных в контексте и входных данных:

import { Hono } from 'hono'

// Тип описывает форму переменных окружения и контекста
type Bindings = {
  DB_URL: string
  API_KEY: string
}

type Variables = {
  userId: number
  role: 'admin' | 'user'
}

const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()

app.use('*', async (c, next) => {
  // c.env.DB_URL и c.env.API_KEY — типизированы
  console.log(c.env.DB_URL)
  
  // Устанавливаем значение в контексте
  c.set('userId', 42)
  c.set('role', 'admin')
  await next()
})

app.get('/profile', (c) => {
  const userId = c.get('userId') // тип: number
  const role = c.get('role')     // тип: 'admin' | 'user'
  return c.json({ userId, role })
})

Валидация с Zod

Hono предоставляет middleware-валидатор @hono/zod-validator, который связывает схему Zod с типами запроса. Это главная киллер-фича фреймворка — тело запроса, параметры и query-строка получают точные TypeScript-типы.

npm install zod @hono/zod-validator
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const app = new Hono()

const userSchema = z.object({
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().min(18).optional(),
})

// Валидация тела запроса
app.post('/users', zValidator('json', userSchema), (c) => {
  // data полностью типизирована по схеме Zod
  const data = c.req.valid('json')
  // data: { name: string; email: string; age?: number }
  
  return c.json({ created: data }, 201)
})

// Валидация query-параметров
const querySchema = z.object({
  page: z.string().regex(/^\d+$/).transform(Number).default('1'),
  limit: z.string().regex(/^\d+$/).transform(Number).default('10'),
})

app.get('/users', zValidator('query', querySchema), (c) => {
  const { page, limit } = c.req.valid('query')
  // page: number, limit: number
  
  return c.json({ page, limit, data: [] })
})

При невалидных данных Hono автоматически вернёт ответ 400 Bad Request с описанием ошибок валидации.

Middleware

Middleware в Hono — это асинхронные функции с доступом к Context и функции next. Типизация работает сквозь всю цепочку.

import { Hono } from 'hono'
import { logger } from 'hono/logger'
import { cors } from 'hono/cors'
import { bearerAuth } from 'hono/bearer-auth'

const app = new Hono()

// Встроенные middleware
app.use('*', logger())
app.use('/api/*', cors())

// Кастомный middleware для аутентификации
app.use('/api/*', async (c, next) => {
  const token = c.req.header('Authorization')?.replace('Bearer ', '')
  
  if (!token) {
    return c.json({ error: 'Unauthorized' }, 401)
  }
  
  // Декодируем токен и сохраняем данные пользователя
  c.set('userId', 123) // тип выводится из Variables
  await next()
})

// Middleware для замера времени
app.use('*', async (c, next) => {
  const start = Date.now()
  await next()
  const duration = Date.now() - start
  c.res.headers.set('X-Response-Time', `${duration}ms`)
})

Группировка маршрутов

Hono поддерживает вложенные роутеры, что позволяет разбить приложение на модули с полным сохранением типов:

import { Hono } from 'hono'

// users.ts
const usersRouter = new Hono()

usersRouter.get('/', (c) => c.json([{ id: 1, name: 'Alice' }]))
usersRouter.get('/:id', (c) => {
  const id = c.req.param('id')
  return c.json({ id, name: 'Alice' })
})
usersRouter.post('/', async (c) => {
  const body = await c.req.json()
  return c.json(body, 201)
})

// posts.ts
const postsRouter = new Hono()

postsRouter.get('/', (c) => c.json([]))
postsRouter.post('/', async (c) => {
  const body = await c.req.json()
  return c.json(body, 201)
})

// app.ts — основное приложение
const app = new Hono()

app.route('/users', usersRouter)
app.route('/posts', postsRouter)

export default app

RPC — типизированный клиент

Одна из уникальных возможностей Hono — генерация типизированного клиента прямо из определения роутера. Серверные и клиентские типы синхронизированы без кодогенерации.

// server.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'

const app = new Hono()
  .get('/books', (c) => {
    return c.json([
      { id: 1, title: 'TypeScript Deep Dive' },
      { id: 2, title: 'Clean Code' },
    ])
  })
  .post(
    '/books',
    zValidator('json', z.object({ title: z.string() })),
    async (c) => {
      const { title } = c.req.valid('json')
      return c.json({ id: 3, title }, 201)
    }
  )

export type AppType = typeof app
export default app
// client.ts
import { hc } from 'hono/client'
import type { AppType } from './server'

const client = hc<AppType>('http://localhost:3000')

async function main() {
  // Полная автодополнение и проверка типов
  const res = await client.books.$get()
  const books = await res.json()
  // books: { id: number; title: string }[]
  
  const newBook = await client.books.$post({
    json: { title: 'Hono in Action' },
  })
  const created = await newBook.json()
  // created: { id: number; title: string }
}

main()

Этот паттерн называется RPC (Remote Procedure Call) и устраняет необходимость в отдельных инструментах вроде OpenAPI-кодогенерации или tRPC.

Обработка ошибок

import { Hono } from 'hono'
import { HTTPException } from 'hono/http-exception'

const app = new Hono()

// Бросаем типизированные HTTP-исключения
app.get('/users/:id', async (c) => {
  const id = Number(c.req.param('id'))
  
  if (isNaN(id)) {
    throw new HTTPException(400, { message: 'Invalid user ID' })
  }
  
  const user = await getUserById(id)
  
  if (!user) {
    throw new HTTPException(404, { message: 'User not found' })
  }
  
  return c.json(user)
})

// Глобальный обработчик ошибок
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json(
      { error: err.message },
      err.status
    )
  }
  
  console.error(err)
  return c.json({ error: 'Internal Server Error' }, 500)
})

// Обработчик 404
app.notFound((c) => {
  return c.json({ error: 'Not Found' }, 404)
})

async function getUserById(id: number) {
  // Имитация запроса к БД
  return id === 1 ? { id: 1, name: 'Alice' } : null
}

Полный пример REST API

Соберём всё вместе — типизированный REST API с валидацией, аутентификацией и обработкой ошибок:

import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { HTTPException } from 'hono/http-exception'
import { logger } from 'hono/logger'
import { z } from 'zod'

type Variables = {
  currentUserId: number
}

const app = new Hono<{ Variables: Variables }>()

// Глобальное логирование
app.use('*', logger())

// --- Схемы ---

const createTaskSchema = z.object({
  title: z.string().min(1).max(200),
  description: z.string().optional(),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
})

const updateTaskSchema = createTaskSchema.partial()

const querySchema = z.object({
  priority: z.enum(['low', 'medium', 'high']).optional(),
  page: z.coerce.number().int().positive().default(1),
})

// --- Хранилище (заглушка) ---

type Task = {
  id: number
  title: string
  description?: string
  priority: 'low' | 'medium' | 'high'
  createdAt: string
}

const tasks: Task[] = [
  { id: 1, title: 'Изучить Hono', priority: 'high', createdAt: new Date().toISOString() },
]
let nextId = 2

// --- Middleware аутентификации ---

app.use('/api/*', async (c, next) => {
  const token = c.req.header('X-User-Id')
  if (!token) {
    throw new HTTPException(401, { message: 'Authentication required' })
  }
  c.set('currentUserId', Number(token))
  await next()
})

// --- Маршруты ---

const api = new Hono<{ Variables: Variables }>()

api.get('/tasks', zValidator('query', querySchema), (c) => {
  const { priority, page } = c.req.valid('query')
  const pageSize = 10
  
  let result = tasks
  if (priority) {
    result = tasks.filter((t) => t.priority === priority)
  }
  
  const paginated = result.slice((page - 1) * pageSize, page * pageSize)
  return c.json({ data: paginated, total: result.length, page })
})

api.post('/tasks', zValidator('json', createTaskSchema), (c) => {
  const input = c.req.valid('json')
  const task: Task = {
    id: nextId++,
    ...input,
    createdAt: new Date().toISOString(),
  }
  tasks.push(task)
  return c.json(task, 201)
})

api.get('/tasks/:id', (c) => {
  const id = Number(c.req.param('id'))
  const task = tasks.find((t) => t.id === id)
  if (!task) {
    throw new HTTPException(404, { message: 'Task not found' })
  }
  return c.json(task)
})

api.patch('/tasks/:id', zValidator('json', updateTaskSchema), (c) => {
  const id = Number(c.req.param('id'))
  const idx = tasks.findIndex((t) => t.id === id)
  if (idx === -1) {
    throw new HTTPException(404, { message: 'Task not found' })
  }
  const updates = c.req.valid('json')
  tasks[idx] = { ...tasks[idx], ...updates }
  return c.json(tasks[idx])
})

api.delete('/tasks/:id', (c) => {
  const id = Number(c.req.param('id'))
  const idx = tasks.findIndex((t) => t.id === id)
  if (idx === -1) {
    throw new HTTPException(404, { message: 'Task not found' })
  }
  tasks.splice(idx, 1)
  return c.body(null, 204)
})

// Подключаем роутер
app.route('/api', api)

// Обработчики ошибок
app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return c.json({ error: err.message }, err.status)
  }
  return c.json({ error: 'Internal Server Error' }, 500)
})

app.notFound((c) => c.json({ error: 'Not Found' }, 404))

export default app

Запуск на Node.js

import { serve } from '@hono/node-server'
import app from './app'

serve({
  fetch: app.fetch,
  port: 3000,
}, (info) => {
  console.log(`Server running at http://localhost:${info.port}`)
})

Тестирование

Hono предоставляет утилиту testClient для юнит-тестирования без поднятия реального сервера:

import { testClient } from 'hono/testing'
import app from './app'

test('GET /api/tasks возвращает список', async () => {
  const client = testClient(app)
  
  const res = await client.api.tasks.$get(
    { query: { page: '1' } },
    { headers: { 'X-User-Id': '1' } }
  )
  
  expect(res.status).toBe(200)
  const body = await res.json()
  expect(Array.isArray(body.data)).toBe(true)
})

test('POST /api/tasks создаёт задачу', async () => {
  const client = testClient(app)
  
  const res = await client.api.tasks.$post(
    { json: { title: 'Test task', priority: 'low' } },
    { headers: { 'X-User-Id': '1' } }
  )
  
  expect(res.status).toBe(201)
  const task = await res.json()
  expect(task.title).toBe('Test task')
})

Когда выбирать Hono

Hono особенно хорошо подходит если:

  • нужен деплой на edge-платформы (Cloudflare Workers, Vercel Edge)
  • важна минимальная задержка и маленький размер бандла
  • требуется сквозная типобезопасность без кодогенерации
  • проект использует Bun или Deno
  • хочется единого API для сервера и клиента через RPC

Для монолитных Node.js-приложений с большой экосистемой плагинов Fastify или NestJS могут быть более подходящим выбором.


Чтобы глубоко разобраться в TypeScript и уверенно использовать его в реальных проектах — пройдите курс TypeScript на PurpleSchool.

Стрелочка влевоIsolated Declarations в TypeScriptФункции в TypeScriptСтрелочка вправо

Постройте личный план изучения Typescript до уровня Middle — бесплатно!

Typescript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Typescript

Переменные и константы в TypeScriptTypeScript: ключевое слово using и Explicit Resource ManagementНастройка типов с declare module в TypeScriptTypeScript типизация в Node.js проектеTypeScript ESLint — настройка линтера для проектаПреобразование к типу в TypeScript (Type Assertion)TypeScript 5: новые возможности и измененияКортежи в TypeScripttsconfig.json — полный разбор настроек TypeScript компилятораTypeScript с tRPC — типобезопасный APITypeScript strict и strictNullChecks — строгий режимБезопасные env переменные в TypeScriptКак собрать TypeScript проект с RollupОбъекты в TypeScriptMonorepo с Turborepo и TypeScriptIsolated Declarations в TypeScriptTypeScript с Hono: типобезопасный веб-фреймворкФункции в TypeScriptТип функции и стрелочные функции в TypeScriptТипизация ошибок и обработка исключений в TypeScriptTypeScript ключевое слово declareМассивы в TypeScript
TypeScript с Zod: генерация и валидация типовКак правильно использовать тип UnknownИспользование Union в TypeScriptТипы данных в TypeScriptType Guards в TypeScript — как работает сужение типовSymbols в TypeScriptСтруктурная типизация в TypeScriptNull и Undefined в TypeScriptТип never в TypeScriptСужение типов (Narrowing) в TypeScriptTypeScript с GraphQL — типизация и кодогенерацияПонимание Literal Types. Ключевые аспекты и примеры использованияОператор infer в Typescript?Типы функций в TypeScript. Руководство для разработчиковTypeScript с Express.js: типизация маршрутов и middlewareEnums vs Union Types в TypeScript: сравнение и выборEnums в TypeScriptДискриминированные объединения в TypeScriptСлужебный тип Awaited в TypeScriptКак типизировать массивы TypeScriptКак правильно использовать тип any
Вариативные кортежи (Variadic Tuple Types) в TypeScriptUtility Types в TypeScript — полный разбор встроенных утилитТипизация ответов API с fetch и generic-функцииtypeof в TypeScriptTemplate Literal Types в TypeScriptПаттерн Result для обработки ошибок без исключенийОператор satisfies в TypeScriptMapped Types в TypeScriptУтилитный тип NoInfer в TypeScriptОператор типа keyof в TypeScriptРуководство по тестированию TypeScript с JestТипы доступа по индексу в TypeScriptGenerics в TypeScriptconst assertions в TypeScriptУсловные типы в TypeScriptBranded Types в TypeScript — номинальная типизацияПродвинутые Conditional Types в TypeScript
Открыть базу знаний

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

CSS Flexbox

Антон Ларичев
Гарантия
Бонусы
иконка звёздочки рейтинга4.9
бесплатно
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий