Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

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 routes

AppType — только тип: в собранный 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 /postsclient.posts.$get()
POST /postsclient.posts.$post({ json })
GET /posts/:idclient.posts[':id'].$get({ param })
GET /search?q=client.search.$get({ query })

Частая ошибка

Маршруты добавляют отдельными строками posts.get(...), а потом удивляются, что у client.posts нет методов. Тип переменной posts фиксируется в момент создания и не меняется от последующих вызовов. Для RPC пишите маршруты цепочкой и экспортируйте тип результата цепочки.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.