Skip to main content

Hono Client

Zelt generates type-safe client types (AppType) for Hono's hc client — enabling fully type-safe API calls with IDE autocomplete.

Overview​

The @zeltjs/hono-client package generates AppType from your controller signatures. This type integrates with Hono's hc client to provide:

  • Full TypeScript inference for request parameters and response bodies
  • IDE autocomplete for API endpoints
  • Compile-time type checking for API calls

Installation​

pnpm add @zeltjs/hono-client

Configuration (CLI Plugin)​

Add honoClientPlugin to your zelt.config.ts:

export default defineConfig({
  entry: './dist/app.js',
  plugins: [
    honoClientPlugin({
      outDir: './generated',
      output: 'app-type.ts',
    }),
  ],
});

Plugin Options​

OptionTypeDefaultDescription
entrystringconfig.entryPath to built app module
outDirstring'./generated'Output directory
outputstring'app-type.ts'Output filename

Generating AppType​

Run zelt build to generate the type file:

pnpm zelt build

This generates <outDir>/<output> (default: generated/app-type.ts) containing the AppType.

Programmatic Generation​

When your build is not driven by the Zelt CLI — an electron-vite pipeline, a custom build script, a monorepo task runner — use GeneratorService.generateFromApp() directly. It takes the http feature of your app and returns the generated type source as a string:

const generator = new GeneratorService();
const content = await generator.generateFromApp(app.http, { distDir: './dist' });

writeFileSync('./dist/app-type.generated.ts', content, 'utf-8');

Run this as a build step after your app modules are compiled — for example a script executed by node against the built output, or an electron-vite buildStart hook.

Generate Options​

OptionTypeDescription
distDirstringDirectory of the built app output; generated imports are resolved against it
portablebooleanEmit a self-contained type file with resolved literal types instead of imports into distDir
tsconfigstringPath to the project tsconfig (required when portable: true)
projectRootstringProject root for path resolution (required when portable: true)

Use portable: true when the generated file is consumed outside the server package — for example an Electron renderer or a separate frontend workspace that cannot import from the server's dist.

Generated app-type.ts​

// THIS FILE IS GENERATED BY @zeltjs/hono-client. DO NOT EDIT.

export type AppType = BuildAppType<[
  Route<'GET', '/hello/:name', () => { message: string }>,
  Route<'POST', '/hello', (input: { name: string }) => { id: string }>,
]>;

Using AppType​

Type-Safe API Client​

const client = hc<AppType>('https://api.example.com');

// Fully typed - IDE autocomplete and type checking
const response = await client.hello[':name'].$get({
  param: { name: 'world' },
});

if (response.ok) {
  const data = await response.json();
  // data is typed as { message: string }
  console.log(data.message);
}

Testing with Type-Safe Client​

describe('Hello API', () => {
  const client = hc<AppType>('http://localhost', {
    fetch: (input, init) => readyApp.http.fetch(new Request(input, init)),
  });

  it('should return greeting', async () => {
    const res = await client.hello[':name'].$get({
      param: { name: 'world' },
    });

    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.message).toBe('Hello, world!');
  });
});

How It Works​

  1. Metadata Extraction — Reads route metadata from your Zelt app at build time
  2. Type Generation — Generates AppType from the extracted route information
  3. Client Integration — The generated type integrates with Hono's hc client

The generated AppType maps your controller routes to Hono's route types, enabling the hc client to infer parameter and response types automatically.

Mixed success and error replies​

Return plain data for JSON 200 and use response() for an explicit status. The plugin preserves each status/body pair instead of wrapping the whole union as JSON 200. The server reply data does not depend on Hono; this package translates it to Hono's client schema.

import { response } from '@zeltjs/core';

function findUser(found: boolean, res = response()) {
  if (!found) return res.status(404).json({ code: 'NOT_FOUND' });
  return { id: 'user-1' };
}

A generated client sees status 200 | 404 and narrows res.json() to the matching body. JSON serialization is reflected in the client type, including Date values becoming strings. Redirects and empty replies do not declare a JSON body.

Unexpected exceptions and responses introduced by global middleware/error handlers are separate contracts; they are not inferred from the handler's return type.