RPC-клиент hc
Когда фронтенд и сервер написаны на TypeScript, удобно, чтобы клиент знал адреса API и формы ответов. В Hono для этого есть клиент hc из hono/client. Он берёт тип серверного приложения и превращает маршруты в методы объекта: вместо fetch('/posts') вы пишете client.posts.$get(), а TypeScript проверяет параметры и тип ответа. Такой подход называют RPC — вызов удалённой процедуры.
Маршруты цепочкой
Тип строится из цепочки вызовов, поэтому маршруты нужно записать одним выражением. Перепишите начало src/routes/posts.ts:
// src/routes/posts.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { db, type Post } from '../db.js'
import { postSchema } from '../schemas.js'
const posts = new Hono()
.get('/', (c) => {
const rows = db.prepare('SELECT * FROM posts ORDER BY id DESC').all() as Post[]
return c.json(rows)
})
.get('/:id', (c) => {
const post = db.prepare('SELECT * FROM posts WHERE id = ?').get(c.req.param('id')) as Post | undefined
return post ? c.json(post, 200) : c.json({ error: 'Статья не найдена' }, 404)
})
.post('/', zValidator('json', postSchema), (c) => {
const { title, body } = c.req.valid('json')
const result = db.prepare('INSERT INTO posts (title, body) VALUES (?, ?)').run(title, body)
return c.json({ id: Number(result.lastInsertRowid) }, 201)
})
export default postsКаждый метод возвращает приложение с уже добавленным маршрутом, и тип запоминает все маршруты цепочки.
Тип приложения
В src/index.ts сохраните результат app.route() и экспортируйте его тип:
// src/index.ts
const routes = app.route('/posts', posts)
export type AppType = typeof routesAppType — только тип: в собранный JavaScript он не попадает, и клиент не получает код сервера.
Клиент
Клиент обычно живёт во фронтенд-проекте, который импортирует тип из серверного. Для проверки создайте скрипт src/client.ts в том же проекте:
// src/client.ts
import { hc } from 'hono/client'
import type { AppType } from './index.js'
const client = hc<AppType>('http://localhost:3000/')
const listRes = await client.posts.$get()
const list = await listRes.json()
console.log(list.map((post) => post.title))
const created = await client.posts.$post({
json: { title: 'Из RPC-клиента', body: 'Текст статьи' },
})
console.log(created.status) // 201
const one = await client.posts[':id'].$get({ param: { id: '1' } })
if (one.ok) {
const post = await one.json()
console.log(post.title)
}Запустите сервер, а во втором терминале — npx tsx src/client.ts. Скрипт выведет заголовки статей, код 201 и заголовок первой статьи. Если ошибиться в адресе или забыть поле body, TypeScript подчеркнёт ошибку ещё до запуска.
Как называются методы
| Запрос | Вызов клиента |
|---|---|
GET /posts | client.posts.$get() |
POST /posts | client.posts.$post({ json }) |
GET /posts/:id | client.posts[':id'].$get({ param }) |
GET /search?q= | client.search.$get({ query }) |
Частая ошибка
Маршруты добавляют отдельными строками posts.get(...), а потом удивляются, что у client.posts нет методов. Тип переменной posts фиксируется в момент создания и не меняется от последующих вызовов. Для RPC пишите маршруты цепочкой и экспортируйте тип результата цепочки.