RPC
Die RPC-Funktion ermöglicht es, API-Spezifikationen zwischen Server und Client zu teilen.
Exportiere zunächst aus deinem Servercode den mit typeof ermittelten Typ deiner Hono-Anwendung (üblicherweise AppType genannt) oder nur den Typ der Routen, die dem Client zur Verfügung stehen sollen.
Indem der Hono-Client AppType als generischen Parameter entgegennimmt, kann er sowohl die vom Validator angegebenen Eingabetypen als auch die Ausgabetypen der Handler inferieren, die c.json() zurückgeben.
NOTE
Damit die RPC-Typen in einem Monorepo korrekt funktionieren, setze in den tsconfig.json-Dateien sowohl des Clients als auch des Servers unter compilerOptions den Wert "strict": true. Weitere Informationen.
Server
Auf der Serverseite musst du lediglich einen Validator schreiben und eine Variable route erstellen. Das folgende Beispiel verwendet den Zod-Validator.
const route = app.post(
'/posts',
zValidator(
'form',
z.object({
title: z.string(),
body: z.string(),
})
),
(c) => {
// ...
return c.json(
{
ok: true,
message: 'Created!',
},
201
)
}
)TIP
Der Standard-Schema-Validator funktioniert ebenfalls. Du kannst also jede Standard-Schema-Bibliothek verwenden, zum Beispiel Valibot.
Exportiere dann den Typ, um die API-Spezifikation mit dem Client zu teilen.
export type AppType = typeof routeClient
Importiere auf der Clientseite zunächst hc und AppType.
import type { AppType } from '.'
import { hc } from 'hono/client'hc ist eine Funktion zur Erstellung eines Clients. Übergib AppType als generischen Typ und gib die Server-URL als Argument an.
const client = hc<AppType>('http://localhost:8787/')Rufe client.{path}.{method} auf und übergib die Daten, die du an den Server senden möchtest, als Argument.
const res = await client.posts.$post({
form: {
title: 'Hello',
body: 'Hono is a cool project',
},
})res ist mit der Response von fetch kompatibel. Mit res.json() kannst du Daten vom Server abrufen.
if (res.ok) {
const data = await res.json()
console.log(data.message)
}Cookies
Damit der Client bei jeder Anfrage Cookies sendet, füge beim Erstellen des Clients { 'init': { 'credentials": 'include' } } zu den Optionen hinzu.
// client.ts
const client = hc<AppType>('http://localhost:8787/', {
init: {
credentials: 'include',
},
})
// This request will now include any cookies you might have set
const res = await client.posts.$get({
query: {
id: '123',
},
})Statuscode
Wenn du den Statuscode, zum Beispiel 200 oder 404, explizit in c.json() angibst, wird er als Typinformation an den Client weitergegeben.
// server.ts
const app = new Hono().get(
'/posts',
zValidator(
'query',
z.object({
id: z.string(),
})
),
async (c) => {
const { id } = c.req.valid('query')
const post: Post | undefined = await getPost(id)
if (post === undefined) {
return c.json({ error: 'not found' }, 404) // Specify 404
}
return c.json({ post }, 200) // Specify 200
}
)
export type AppType = typeof appDu kannst die Daten abhängig vom Statuscode abrufen.
// client.ts
const client = hc<AppType>('http://localhost:8787/')
const res = await client.posts.$get({
query: {
id: '123',
},
})
if (res.status === 404) {
const data: { error: string } = await res.json()
console.log(data.error)
}
if (res.ok) {
const data: { post: Post } = await res.json()
console.log(data.post)
}
// { post: Post } | { error: string }
type ResponseType = InferResponseType<typeof client.posts.$get>
// { post: Post }
type ResponseType200 = InferResponseType<
typeof client.posts.$get,
200
>Globale Antwort
Der Hono-RPC-Client inferiert Antworttypen aus globalen Fehler-Handlern wie app.onError() oder globaler Middleware nicht automatisch. Mit dem Typ-Helper ApplyGlobalResponse kannst du globale Fehlerantworttypen in alle Routen integrieren.
import type { ApplyGlobalResponse } from 'hono/client'
const app = new Hono()
.get('/api/users', (c) => c.json({ users: ['alice', 'bob'] }, 200))
.onError((err, c) => c.json({ error: err.message }, 500))
type AppWithErrors = ApplyGlobalResponse<
typeof app,
{
500: { json: { error: string } }
}
>
const client = hc<AppWithErrors>('http://localhost')Jetzt kennt der Client sowohl Erfolgs- als auch Fehlerantworten:
const res = await client.api.users.$get()
if (res.ok) {
const data = await res.json() // { users: string[] }
}
// InferResponseType includes the global error type
type ResType = InferResponseType<typeof client.api.users.$get>
// { users: string[] } | { error: string }Du kannst auch mehrere globale Fehlerstatuscodes gleichzeitig definieren:
type AppWithErrors = ApplyGlobalResponse<
typeof app,
{
401: { json: { error: string; message: string } }
500: { json: { error: string; message: string } }
}
>Nicht gefunden
Wenn du einen Client verwenden möchtest, solltest du für eine Nicht-gefunden-Antwort nicht c.notFound() verwenden. Die Daten, die der Client vom Server erhält, lassen sich dann nicht korrekt inferieren.
// server.ts
export const routes = new Hono().get(
'/posts',
zValidator(
'query',
z.object({
id: z.string(),
})
),
async (c) => {
const { id } = c.req.valid('query')
const post: Post | undefined = await getPost(id)
if (post === undefined) {
return c.notFound() // ❌️
}
return c.json({ post })
}
)
// client.ts
import { hc } from 'hono/client'
const client = hc<typeof routes>('/')
const res = await client.posts[':id'].$get({
param: {
id: '123',
},
})
const data = await res.json() // 🙁 data is unknownVerwende bitte c.json() und gib den Statuscode für die Nicht-gefunden-Antwort an.
export const routes = new Hono().get(
'/posts',
zValidator(
'query',
z.object({
id: z.string(),
})
),
async (c) => {
const { id } = c.req.valid('query')
const post = await getPost(id)
if (!post) {
return c.json({ error: 'not found' }, 404) // Specify 404
}
return c.json({ post }, 200) // Specify 200
}
)Alternativ kannst du die Schnittstelle NotFoundResponse mithilfe von Modulerweiterung ergänzen. So kann c.notFound() eine typisierte Antwort zurückgeben:
// server.ts
import { Hono, TypedResponse } from 'hono'
declare module 'hono' {
interface NotFoundResponse
extends Response,
TypedResponse<{ error: string }, 404, 'json'> {}
}
const app = new Hono()
.get('/posts/:id', async (c) => {
const post = await getPost(c.req.param('id'))
if (!post) {
return c.notFound()
}
return c.json({ post }, 200)
})
.notFound((c) => c.json({ error: 'not found' }, 404))
export type AppType = typeof appJetzt kann der Client den Typ der 404-Antwort korrekt inferieren.
Pfadparameter
Du kannst auch Routen mit Pfadparametern oder Abfragewerten verarbeiten.
const route = app.get(
'/posts/:id',
zValidator(
'query',
z.object({
page: z.coerce.number().optional(), // coerce to convert to number
})
),
(c) => {
// ...
return c.json({
title: 'Night',
body: 'Time to sleep',
})
}
)Sowohl Pfadparameter als auch Abfragewerte müssen als string übergeben werden, selbst wenn der zugrunde liegende Wert einen anderen Typ hat.
Gib mit param die Zeichenfolge an, die du in den Pfad aufnehmen möchtest, und mit query die Abfragewerte.
const res = await client.posts[':id'].$get({
param: {
id: '123',
},
query: {
page: '1', // `string`, converted by the validator to `number`
},
})Mehrere Parameter
Verarbeite Routen mit mehreren Parametern.
const route = app.get(
'/posts/:postId/:authorId',
zValidator(
'query',
z.object({
page: z.string().optional(),
})
),
(c) => {
// ...
return c.json({
title: 'Night',
body: 'Time to sleep',
})
}
)Füge mehrere [''] hinzu, um die Parameter im Pfad anzugeben.
const res = await client.posts[':postId'][':authorId'].$get({
param: {
postId: '123',
authorId: '456',
},
query: {},
})Schrägstriche einschließen
Die Funktion hc führt für die Werte von param keine URL-Kodierung durch. Verwende reguläre Ausdrücke, um Schrägstriche in Parametern zuzulassen.
// client.ts
// Requests /posts/123/456
const res = await client.posts[':id'].$get({
param: {
id: '123/456',
},
})
// server.ts
const route = app.get(
'/posts/:id{.+}',
zValidator(
'param',
z.object({
id: z.string(),
})
),
(c) => {
// id: 123/456
const { id } = c.req.valid('param')
// ...
}
)NOTE
Einfache Pfadparameter ohne reguläre Ausdrücke erfassen keine Schrägstriche. Wenn du über die Funktion hc einen param mit Schrägstrichen übergibst, entspricht das Routing des Servers möglicherweise nicht deinen Erwartungen. Wir empfehlen, die Parameter mit encodeURIComponent zu kodieren, um korrektes Routing sicherzustellen.
Header
Du kannst der Anfrage Header hinzufügen.
const res = await client.search.$get(
{
//...
},
{
headers: {
'X-Custom-Header': 'Here is Hono Client',
'X-User-Agent': 'hc',
},
}
)Um allen Anfragen einen gemeinsamen Header hinzuzufügen, gib ihn als Argument der Funktion hc an.
const client = hc<AppType>('/api', {
headers: {
Authorization: 'Bearer TOKEN',
},
})Option init
Du kannst das RequestInit-Objekt von fetch als Option init an die Anfrage übergeben. Das folgende Beispiel zeigt das Abbrechen einer Anfrage.
import { hc } from 'hono/client'
const client = hc<AppType>('http://localhost:8787/')
const abortController = new AbortController()
const res = await client.api.posts.$post(
{
json: {
// Request body
},
},
{
// RequestInit object
init: {
signal: abortController.signal,
},
}
)
// ...
abortController.abort()Info
Ein über init definiertes RequestInit-Objekt hat die höchste Priorität. Damit kannst du Einstellungen überschreiben, die durch andere Optionen wie body | method | headers gesetzt wurden.
$url()
Mit $url() kannst du ein URL-Objekt zum Zugriff auf den Endpunkt abrufen.
Achtung
Damit dies funktioniert, musst du eine absolute URL übergeben. Wenn du die relative URL / übergibst, tritt folgender Fehler auf.
Uncaught TypeError: Failed to construct 'URL': Invalid URL
// ❌ Will throw error
const client = hc<AppType>('/')
client.api.post.$url()
// ✅ Will work as expected
const client = hc<AppType>('http://localhost:8787/')
client.api.post.$url()const route = app
.get('/api/posts', (c) => c.json({ posts }))
.get('/api/posts/:id', (c) => c.json({ post }))
const client = hc<typeof route>('http://localhost:8787/')
let url = client.api.posts.$url()
console.log(url.pathname) // `/api/posts`
url = client.api.posts[':id'].$url({
param: {
id: '123',
},
})
console.log(url.pathname) // `/api/posts/123`Typisierte URL
Du kannst die Basis-URL als zweiten Typparameter an hc übergeben, um präzisere URL-Typen zu erhalten:
const client = hc<typeof route, 'http://localhost:8787'>(
'http://localhost:8787/'
)
const url = client.api.posts.$url()
// url is TypedURL with precise type information
// including protocol, host, and pathDas ist hilfreich, wenn du die URL als typsicheren Schlüssel für Bibliotheken wie SWR verwenden möchtest.
$path()
$path() ähnelt $url(), gibt aber statt eines URL-Objekts eine Pfadzeichenfolge zurück. Anders als $url() enthält es nicht den Origin der Basis-URL und funktioniert deshalb unabhängig davon, welche Basis-URL du an hc übergibst.
const route = app
.get('/api/posts', (c) => c.json({ posts }))
.get('/api/posts/:id', (c) => c.json({ post }))
const client = hc<typeof route>('http://localhost:8787/')
let path = client.api.posts.$path()
console.log(path) // `/api/posts`
path = client.api.posts[':id'].$path({
param: {
id: '123',
},
})
console.log(path) // `/api/posts/123`Du kannst auch Abfrageparameter übergeben:
const path = client.api.posts.$path({
query: {
page: '1',
limit: '10',
},
})
console.log(path) // `/api/posts?page=1&limit=10`Datei-Uploads
Du kannst Dateien mit einem Formular-Body hochladen:
// client
const res = await client.user.picture.$put({
form: {
file: new File([fileToUpload], filename, {
type: fileToUpload.type,
}),
},
})// server
const route = app.put(
'/user/picture',
zValidator(
'form',
z.object({
file: z.instanceof(File),
})
)
// ...
)Eigene fetch-Methode
Du kannst eine eigene fetch-Methode festlegen.
Im folgenden Beispielskript für einen Cloudflare Worker wird die Methode fetch des Service Bindings anstelle des standardmäßigen fetch verwendet.
# wrangler.toml
services = [
{ binding = "AUTH", service = "auth-service" },
]// src/client.ts
const client = hc<CreateProfileType>('http://localhost', {
fetch: c.env.AUTH.fetch.bind(c.env.AUTH),
})Eigener Abfrage-Serialisierer
Mit der Option buildSearchParams kannst du anpassen, wie Abfrageparameter serialisiert werden. Das ist nützlich, wenn du die Schreibweise mit eckigen Klammern für Arrays oder andere eigene Formate benötigst:
const client = hc<AppType>('http://localhost', {
buildSearchParams: (query) => {
const searchParams = new URLSearchParams()
for (const [k, v] of Object.entries(query)) {
if (v === undefined) {
continue
}
if (Array.isArray(v)) {
v.forEach((item) => searchParams.append(`${k}[]`, item))
} else {
searchParams.set(k, v)
}
}
return searchParams
},
})Typen inferieren
Mit InferRequestType und InferResponseType kannst du den Typ des zu sendenden und des zurückgegebenen Objekts ermitteln.
import type { InferRequestType, InferResponseType } from 'hono/client'
// InferRequestType
const $post = client.todo.$post
type ReqType = InferRequestType<typeof $post>['form']
// InferResponseType
type ResType = InferResponseType<typeof $post>Eine Antwort mit einem typsicheren Helper parsen
Mit dem Helper parseResponse() kannst du eine Response von hc einfach und typsicher parsen.
import { parseResponse, DetailedError } from 'hono/client'
// result contains the parsed response body (automatically parsed based on Content-Type)
const result = await parseResponse(client.hello.$get()).catch(
(e: DetailedError) => {
console.error(e)
}
)
// parseResponse automatically throws an error if response is not okSWR verwenden
Du kannst auch eine React-Hook-Bibliothek wie SWR verwenden.
import useSWR from 'swr'
import { hc } from 'hono/client'
import type { InferRequestType } from 'hono/client'
import type { AppType } from '../functions/api/[[route]]'
const App = () => {
const client = hc<AppType>('/api')
const $get = client.hello.$get
const fetcher =
(arg: InferRequestType<typeof $get>) => async () => {
const res = await $get(arg)
return await res.json()
}
const { data, error, isLoading } = useSWR(
'api-hello',
fetcher({
query: {
name: 'SWR',
},
})
)
if (error) return <div>failed to load</div>
if (isLoading) return <div>loading...</div>
return <h1>{data?.message}</h1>
}
export default AppRPC in größeren Anwendungen verwenden
Bei einer größeren Anwendung, wie dem Beispiel aus Eine größere Anwendung erstellen, musst du auf die Typinferenz achten. Eine einfache Vorgehensweise ist, die Handler zu verketten, damit die Typen immer inferiert werden.
// authors.ts
import { Hono } from 'hono'
const app = new Hono()
.get('/', (c) => c.json('list authors'))
.post('/', (c) => c.json('create an author', 201))
.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
export default app// books.ts
import { Hono } from 'hono'
const app = new Hono()
.get('/', (c) => c.json('list books'))
.post('/', (c) => c.json('create a book', 201))
.get('/:id', (c) => c.json(`get ${c.req.param('id')}`))
export default appAnschließend kannst du die Unterrouter wie gewohnt importieren und auch deren Handler verketten. Da dies in diesem Fall die oberste Ebene der Anwendung ist, möchten wir diesen Typ exportieren.
// index.ts
import { Hono } from 'hono'
import authors from './authors'
import books from './books'
const app = new Hono()
const routes = app.route('/authors', authors).route('/books', books)
export default app
export type AppType = typeof routesDu kannst nun mit dem registrierten AppType einen neuen Client erstellen und ihn wie gewohnt verwenden.
Bekannte Probleme
IDE-Leistung
Bei der Verwendung von RPC wird deine IDE langsamer, je mehr Routen du hast. Ein wesentlicher Grund dafür ist, dass sehr viele Typinstanziierungen ausgeführt werden, um den Typ deiner Anwendung zu inferieren.
Angenommen, deine Anwendung hat zum Beispiel diese Route:
// app.ts
export const app = new Hono().get('foo/:id', (c) =>
c.json({ ok: true }, 200)
)Hono inferiert den Typ wie folgt:
export const app = Hono<BlankEnv, BlankSchema, '/'>().get<
'foo/:id',
'foo/:id',
JSONRespondReturn<{ ok: boolean }, 200>,
BlankInput,
BlankEnv
>('foo/:id', (c) => c.json({ ok: true }, 200))Dies ist eine Typinstanziierung für eine einzelne Route. Dass du diese Typargumente nicht manuell schreiben musst, ist praktisch. Allerdings benötigen Typinstanziierungen bekanntermaßen viel Zeit. Der in deiner IDE verwendete tsserver führt diese zeitaufwendige Aufgabe bei jeder Verwendung der Anwendung aus. Bei vielen Routen kann das deine IDE erheblich verlangsamen.
Wir haben jedoch einige Tipps, um dieses Problem abzumildern.
Unterschiedliche Hono-Versionen
Wenn dein Backend vom Frontend getrennt ist und in einem anderen Verzeichnis liegt, musst du sicherstellen, dass die Hono-Versionen übereinstimmen. Verwendest du im Backend eine andere Hono-Version als im Frontend, können Probleme wie „Type instantiation is excessively deep and possibly infinite“ auftreten, also eine zu tiefe und möglicherweise unendliche Typinstanziierung.
TypeScript-Projektreferenzen
Wie bei unterschiedlichen Hono-Versionen können Probleme auftreten, wenn Backend und Frontend getrennt sind. Wenn du im Frontend auf Backend-Code zugreifen möchtest (zum Beispiel auf AppType), musst du Projektreferenzen verwenden. Mit TypeScript-Projektreferenzen kann eine TypeScript-Codebasis auf Code einer anderen TypeScript-Codebasis zugreifen und ihn verwenden. (Quelle: Hono RPC und TypeScript-Projektreferenzen).
Code vor der Verwendung kompilieren (empfohlen)
tsc kann aufwendige Aufgaben wie die Typinstanziierung bereits zur Kompilierzeit erledigen! Dann muss tsserver nicht bei jeder Verwendung alle Typargumente instanziieren. Das macht deine IDE deutlich schneller!
Die beste Leistung erhältst du, wenn du deinen Client einschließlich der Serveranwendung kompilierst. Füge folgenden Code in dein Projekt ein:
import { app } from './app'
import { hc } from 'hono/client'
// this is a trick to calculate the type when compiling
const client = hc<typeof app>('')
export type Client = typeof client
export const hcWithType = (...args: Parameters<typeof hc>): Client =>
hc<typeof app>(...args)Nach dem Kompilieren kannst du hcWithType anstelle von hc verwenden, um den Client mit bereits berechnetem Typ zu erhalten.
const client = hcWithType('http://localhost:8787/')
const res = await client.posts.$post({
form: {
title: 'Hello',
body: 'Hono is a cool project',
},
})Für ein Monorepo eignet sich diese Lösung besonders gut. Mit einem Werkzeug wie turborepo kannst du das Server- und das Client-Projekt einfach trennen und ihre Abhängigkeiten besser verwalten. Hier ist ein funktionsfähiges Beispiel.
Du kannst deinen Build-Prozess auch manuell mit Werkzeugen wie concurrently oder npm-run-all koordinieren.
Typargumente manuell angeben
Das ist etwas umständlich, aber du kannst Typargumente manuell angeben, um Typinstanziierungen zu vermeiden.
const app = new Hono().get<'foo/:id'>('foo/:id', (c) =>
c.json({ ok: true }, 200)
)Schon die Angabe eines einzelnen Typarguments kann die Leistung verbessern. Bei vielen Routen kann dies jedoch viel Zeit und Aufwand erfordern.
Anwendung und Client auf mehrere Dateien aufteilen
Wie unter RPC in größeren Anwendungen verwenden beschrieben, kannst du deine Anwendung in mehrere Anwendungen aufteilen. Du kannst auch für jede Anwendung einen Client erstellen:
// authors-cli.ts
import { app as authorsApp } from './authors'
import { hc } from 'hono/client'
const authorsClient = hc<typeof authorsApp>('/authors')
// books-cli.ts
import { app as booksApp } from './books'
import { hc } from 'hono/client'
const booksClient = hc<typeof booksApp>('/books')So muss tsserver nicht die Typen aller Routen gleichzeitig instanziieren.
Handler, die eine Promise-Kette zurückgeben
Ein Handler, der direkt eine .then()-Kette zurückgibt, verliert seinen Antworttyp. Deshalb inferiert der Client unknown:
const app = new Hono().get('/', (c) =>
Promise.resolve({ hello: 'world' }).then((d) => c.json(d))
)
const client = hc<typeof app>('')
const res = await client.index.$get()
const data = await res.json() // unknownDas ist eine Einschränkung der TypeScript-Typinferenz: Der Antworttyp kann nicht durch eine .then()-Kette hindurch inferiert werden. Verwende stattdessen async/await:
const app = new Hono().get('/', async (c) => {
const d = await Promise.resolve({ hello: 'world' })
return c.json(d)
})
const client = hc<typeof app>('')
const res = await client.index.$get()
const data = await res.json() // { hello: string }Wenn du die Kette nicht vermeiden kannst, funktioniert auch eine Typannotation für then():
import type { TypedResponse } from 'hono/types'
const app = new Hono().get('/', (c) =>
Promise.resolve({ hello: 'world' }).then<
TypedResponse<{ hello: string }, 200, 'json'>
>((d) => c.json(d, 200))
)