Hono Stacks
Hono makes easy things easy and hard things easy. It is suitable for not just only returning JSON. But it's also great for building the full-stack application including REST API servers and the client.
RPC
Hono's RPC feature allows you to share API specs with little change to your code. The client generated by hc
will read the spec and access the endpoint Type-Safety.
The following libraries make it possible.
- Hono - API Server
- Zod - Validator
- Zod Validator Middleware
hc
- HTTP Client
We can call the set of these components the Hono Stack. Now let's create an API server and a client with it.
Writing API
First, write an endpoint that receives a GET request and returns JSON.
import { Hono } from 'hono'
const app = new Hono()
app.get('/hello', (c) => {
return c.json({
message: `Hello!`,
})
})
Validation with Zod
Validate with Zod to receive the value of the query parameter.
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
app.get(
'/hello',
zValidator(
'query',
z.object({
name: z.string(),
})
),
(c) => {
const { name } = c.req.valid('query')
return c.json({
message: `Hello! ${name}`,
})
}
)
Sharing the Types
To emit an endpoint specification, export its type.
const route = app.get(
'/hello',
zValidator(
'query',
z.object({
name: z.string(),
})
),
(c) => {
const { name } = c.req.valid('query')
return c.json({
message: `Hello! ${name}`,
})
}
)
export type AppType = typeof route
Client
Next. The client-side implementation. Create a client object by passing the AppType type to hc
as generics. Then, magically, completion works and the endpoint path and request type are suggested.
import { AppType } from './server'
import { hc } from 'hono/client'
const client = hc<AppType>('/api')
const res = await client.hello.$get({
query: {
name: 'Hono',
},
})
The Response
is compatible with the fetch API, but the data that can be retrieved with json()
has a type.
const data = await res.json()
console.log(`${data.message}`)
Sharing API specifications means that you can be aware of server-side changes.
With React
You can create applications on Cloudflare Pages using React.
The API server.
// functions/api/[[route]].ts
import { Hono } from 'hono'
import { handle } from 'hono/cloudflare-pages'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator'
const app = new Hono()
const schema = z.object({
id: z.string(),
title: z.string(),
})
type Todo = z.infer<typeof schema>
const todos: Todo[] = []
const route = app
.post('/todo', zValidator('form', schema), (c) => {
const todo = c.req.valid('form')
todos.push(todo)
return c.json({
message: 'created!',
})
})
.get((c) => {
return c.json({
todos,
})
})
export type AppType = typeof route
export const onRequest = handle(app, '/api')
The client with React and React Query.
// src/App.tsx
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { AppType } from '../functions/api/[[route]]'
import { hc, InferResponseType, InferRequestType } from 'hono/client'
const queryClient = new QueryClient()
const client = hc<AppType>('/api')
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>
)
}
const Todos = () => {
const query = useQuery({
queryKey: ['todos'],
queryFn: async () => {
const res = await client.todo.$get()
return await res.json()
},
})
const $post = client.todo.$post
const mutation = useMutation<
InferResponseType<typeof $post>,
Error,
InferRequestType<typeof $post>['form']
>(
async (todo) => {
const res = await $post({
form: todo,
})
return await res.json()
},
{
onSuccess: async () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
onError: (error) => {
console.log(error)
},
}
)
return (
<div>
<button
onClick={() => {
mutation.mutate({
id: Date.now().toString(),
title: 'Write code',
})
}}
>
Add Todo
</button>
<ul>
{query.data?.todos.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
</div>
)
}