DEV Community

Subhendu Pratap Singh
Subhendu Pratap Singh

Posted on

Typed fetch with Sveltekit and Hono using RPC

What is Hono?

Hono is a fast and simple web framework that works in edge environments like cloudflare workers, vercel edge etc. Its API is similar to that of express js, so if you have worked with the express in the past, you can be productive with Hono from day one. If you are writing APIs with Hono for the edge, many of the node APIs like "fs" will not be available in Hono since the edge runtimes don't support them (not completely, atleast).

What is RPC feature and how is it beneficial?

Typically, to make an API call from the frontend, you need to specify:

  1. The base URL + the path of the handler function
  2. The request parameters
  3. The authorization header

These 3 things are specified in the API call according to the specification shared by the API provider. RPC feature handles exactly this. It allows sharing the API specification between the server and the client, such that, while making an API call to the Hono server, the client is already aware about the remote function path, required and optional request params and the headers required for a successful call.

How to use the RPC feature?

A simple API route in Hono looks like this:

import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => c.json({ message: "Hello World" }));
app.get("/hello/:name", (c) =>
   c.json({ message: `Hello ${c.req.param("name")}` })
);

export default app;
Enter fullscreen mode Exit fullscreen mode

This will return json response to the client but the client will have no specification about the path parameter that the routr /hello/:name expects and what is the shape of the response it returns. For the client, to have this specification, we will have to make a few changes to this API route:

import { Hono } from "hono";

const app = new Hono();

const router = app
  .get("/", (c) => c.jsonT({ message: "Hello World" }))
  .get("/hello/:name", (c) =>
    c.jsonT({ message: `Hello ${c.req.param("name")}` })
  );

export default app;

export type AppType = typeof router;
Enter fullscreen mode Exit fullscreen mode

Here instead of the .json response, we return the typed json response .jsonT and we also return the type of the router.

On the client side we can access these routes like this:

import { AppType } from '.'
import { hc } from 'hono/client'

const client = hc<AppType>('http://localhost:8787/')
Enter fullscreen mode Exit fullscreen mode

Here hc is the function to create a typed client of the Hono server. We pass the AppType, the type of the router returned from the Hono server as generics.

Note how AppType is imported here from the server. This is because both the server and the client are a part of one monorepo. Without them being a part of the same monorepo, this is not possible.

Now, we can call our api routes from the client like this

const res = await client.hello[':name'].$get({
  param: {
    name: 'hono',
  },
  query: {},
})
Enter fullscreen mode Exit fullscreen mode

The client here is fully typed and will give intellisense about the parameters and response.

Separating routes in different files

In large backend apps, it is quite common to have multiple routes/controllers in separate files. For example, a file named book.ts may contain all the endpoints related to books and a separate file named author.ts may contain all the endpoints related to the authors. In this case, to share the API specifications using RPC with the client, we can do this:

// book.ts
const book = new Hono()
export const bookRoute = book.get('/', (c) => {
  return c.jsonT({
    books: ['foo'],
  })
})

// author.ts
const author = new Hono()
export const authorRoute = author.get('/', (c) => {
  return c.jsonT({
    authors: ['bar'],
  })
})

// main.ts
const app = new Hono()
const appRoute = app.route('/book', bookRoute).route('/author', authorRoute)
Enter fullscreen mode Exit fullscreen mode

This way, we can share the complete API specification with our frontend app and make sure the API endpoints are typed and the request and response are properly handled.

This was one of the many lessons I learned in the process of migrating my SaaS StoreBud from AWS to Cloudflare workers. I will keep sharing as and when I learn something new.

Top comments (0)