npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@firtoz/hono-fetcher

v2.6.0

Published

Type-safe Hono API client with full TypeScript inference for routes, params, and payloads

Downloads

784

Readme

@firtoz/hono-fetcher

npm version npm downloads license

Type-safe Hono API client with full TypeScript inference for routes, params, and payloads.

⚠️ Early WIP Notice: This package is in very early development and is not production-ready. It is TypeScript-only and may have breaking changes. While I (the maintainer) have limited time, I'm open to PRs for features, bug fixes, or additional support (like JS builds). Please feel free to try it out and contribute! See CONTRIBUTING.md for details.

Features

  • 🔒 Fully Type-Safe - Complete TypeScript inference for routes, parameters, request bodies, and responses
  • 🎯 Path Parameters - Automatic extraction and validation of path parameters (:id, :slug, etc.)
  • 📝 Request Bodies - Type-safe JSON and form data support with automatic serialization
  • 🌐 Cloudflare Workers - First-class support for Durable Objects with honoDoFetcher
  • 🔌 WebSocket Support - Type-safe WebSocket connections with automatic acceptance and configuration
  • 🚀 Zero Runtime Overhead - All type inference happens at compile time
  • 🔄 Full HTTP Methods - Support for GET, POST, PUT, DELETE, and PATCH

Installation

bun add @firtoz/hono-fetcher

Peer Dependencies

This package requires the following peer dependencies:

bun add hono

For Durable Object support, use wrangler types to generate accurate types:

wrangler types

This generates worker-configuration.d.ts with types for your specific environment bindings.

Quick Start

Basic Usage

import { Hono } from 'hono';
import { honoFetcher } from '@firtoz/hono-fetcher';

// Define your Hono app
const app = new Hono()
  .get('/users/:id', (c) => {
    const id = c.req.param('id');
    return c.json({ id, name: `User ${id}` });
  })
  .post('/users', async (c) => {
    const body = await c.req.json<{ name: string }>();
    return c.json({ id: '123', ...body });
  });

// Create a typed fetcher
const api = honoFetcher<typeof app>(app.request);

// Use it with full type safety!
const response = await api.get({
  url: '/users/:id',
  params: { id: '123' }, // ✅ Type-safe params
});

const user = await response.json(); // ✅ Inferred type: { id: string; name: string }

// POST with body
await api.post({
  url: '/users',
  body: { name: 'John' }, // ✅ Type-safe body
});

Remote API Usage

For remote APIs, you have two options:

Option 1: Using honoDirectFetcher (Recommended)

import { honoDirectFetcher } from '@firtoz/hono-fetcher';
import type { AppType } from './backend/app'; // Your backend app type

// Simply pass the base URL
const api = honoDirectFetcher<AppType>('https://api.example.com');

// Use it immediately
const response = await api.get({
  url: '/users/:id',
  params: { id: '123' }
});

Option 2: Using honoFetcher with Custom Fetch

import { honoFetcher } from '@firtoz/hono-fetcher';

// For more control over the fetch behavior
const api = honoFetcher<typeof app>((url, init) => {
  return fetch(`https://api.example.com${url}`, init);
});

Durable Objects

import { honoDoFetcher, honoDoFetcherWithName } from '@firtoz/hono-fetcher';
import { DurableObject } from 'cloudflare:workers';
import { Hono } from 'hono';

// Define your Durable Object with a Hono app
export class ChatRoomDO extends DurableObject {
  app = new Hono()
    .get('/messages', (c) => {
      return c.json({ messages: [] });
    })
    .post('/messages', async (c) => {
      const { text } = await c.req.json<{ text: string }>();
      return c.json({ id: '1', text });
    });

  fetch(request: Request) {
    return this.app.fetch(request);
  }
}

// `using api` disposes the stub. Returning the DO `Response` directly is a common pass-through pattern;
// if you consume the body in this worker instead, also dispose the RPC result (e.g. `using res`).
// See https://developers.cloudflare.com/workers/runtime-apis/rpc/lifecycle/
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    using api = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
    return api.get({ url: '/messages' });
  }
};

API Reference

honoFetcher<T>(fetcher)

Creates a type-safe API client from a Hono app type.

Parameters

  • fetcher: (url: string, init?: RequestInit) => Response | Promise<Response> - Function that performs the actual fetch

Returns

A typed fetcher with methods for each HTTP verb: get, post, put, delete, patch

Example

const api = honoFetcher<typeof app>(app.request);

honoDirectFetcher<T>(baseUrl)

Convenience wrapper around honoFetcher for remote APIs. Automatically prepends the base URL to all requests.

Parameters

  • baseUrl: string - The base URL of your API (e.g., 'https://api.example.com')

Returns

A typed fetcher with methods for each HTTP verb: get, post, put, delete, patch

Example

import { honoDirectFetcher } from '@firtoz/hono-fetcher';
import type { AppType } from './backend/app';

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

// Make requests
const response = await api.get({
  url: '/users/:id',
  params: { id: '123' }
});

Method Signature

All methods follow this signature:

method({
  url: string;           // The route path
  params?: object;       // Path parameters (required if route has :params)
  body?: object;         // Request body (for POST/PUT/PATCH)
  form?: object;         // Form data (for POST/PUT/PATCH)
  init?: RequestInit;    // Additional fetch options
})

Path Parameters

Routes with path parameters (:id, :slug, etc.) require the params field:

const app = new Hono()
  .get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
  .get('/posts/:id/comments/:commentId', (c) => 
    c.json({ 
      postId: c.req.param('id'),
      commentId: c.req.param('commentId') 
    })
  );

const api = honoFetcher<typeof app>(app.request);

// Single parameter
await api.get({
  url: '/users/:id',
  params: { id: '123' } // ✅ Required and type-safe
});

// Multiple parameters
await api.get({
  url: '/posts/:id/comments/:commentId',
  params: { id: '1', commentId: '42' } // ✅ Both required
});

Request Bodies

JSON Bodies

const app = new Hono()
  .post('/users', async (c) => {
    const { name, email } = await c.req.json<{ name: string; email: string }>();
    return c.json({ id: '1', name, email });
  });

const api = honoFetcher<typeof app>(app.request);

await api.post({
  url: '/users',
  body: { name: 'John', email: '[email protected]' } // ✅ Type-safe
});

Form Data

import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';

const app = new Hono()
  .post('/upload', 
    zValidator('form', z.object({
      title: z.string(),
      count: z.coerce.number()
    })),
    async (c) => {
      const data = c.req.valid('form');
      return c.json({ success: true, data });
    }
  );

const api = honoFetcher<typeof app>(app.request);

await api.post({
  url: '/upload',
  form: { title: 'Hello', count: '5' } // ✅ Automatically sent as FormData
});

Custom Headers and Options

Pass additional fetch options via the init parameter:

await api.get({
  url: '/users/:id',
  params: { id: '123' },
  init: {
    headers: {
      'Authorization': 'Bearer token',
      'X-Custom-Header': 'value'
    },
    signal: abortController.signal
  }
});

WebSocket Support

hono-fetcher provides first-class support for WebSocket connections with full type safety.

Basic WebSocket Connection

import { honoFetcher } from '@firtoz/hono-fetcher';

const api = honoFetcher<typeof app>(fetcher);

// Connect to a WebSocket endpoint
const wsResponse = await api.websocket({
  url: '/chat',
});

// Access the WebSocket
const ws = wsResponse.webSocket;
if (ws) {
  ws.send(JSON.stringify({ type: 'hello' }));
  
  ws.addEventListener('message', (event) => {
    console.log('Received:', event.data);
  });
}

WebSocket with Auto-Accept

By default, WebSockets are automatically accepted for convenience:

// Default behavior - WebSocket is auto-accepted
const wsResp = await api.websocket({
  url: '/websocket',
  // config.autoAccept defaults to true
});

// WebSocket is ready to use immediately!
wsResp.webSocket?.send('Hello!');

Manual WebSocket Acceptance

For advanced scenarios where you need control over when the WebSocket is accepted:

const wsResp = await api.websocket({
  url: '/websocket',
  config: { autoAccept: false }, // Disable auto-accept
});

const ws = wsResp.webSocket;
if (ws) {
  // Set up your listeners first
  ws.addEventListener('message', (event) => {
    console.log('Message:', event.data);
  });
  
  // Then manually accept when ready
  ws.accept();
}

WebSocket with Path Parameters

const api = honoFetcher<typeof app>(fetcher);

// WebSocket endpoint with path parameters
const wsResp = await api.websocket({
  url: '/rooms/:roomId/websocket',
  params: { roomId: 'room-123' }, // Type-safe params!
});

Integration with ZodWebSocketClient

For even better type safety, combine with @firtoz/websocket-do's ZodWebSocketClient:

import { ZodWebSocketClient } from '@firtoz/websocket-do';
import { honoDoFetcherWithName } from '@firtoz/hono-fetcher';

// 1. Connect to DO WebSocket
const api = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
const wsResp = await api.websocket({
  url: '/websocket',
  config: { autoAccept: false }, // Let ZodWebSocketClient handle acceptance
});

// 2. Wrap with type-safe client
const client = new ZodWebSocketClient({
  webSocket: wsResp.webSocket,
  clientSchema: ClientMessageSchema,
  serverSchema: ServerMessageSchema,
  onMessage: (message) => {
    // Fully typed message!
    console.log('Received:', message);
  },
});

// 3. Now accept
wsResp.webSocket?.accept();

// 4. Send type-safe messages
client.send({ type: 'chat', text: 'Hello!' }); // Validated with Zod!

See the ZodWebSocketClient documentation for more details on type-safe WebSocket communication.

Durable Objects API

All of these return a fetcher that is also a Disposable for the stub: api[Symbol.dispose]() releases the Durable Object stub only.

RPC Response typing: honoDoFetcherWithName / honoDoFetcherWithId (and honoDoFetcher when you pass a full DurableObjectStub) use TypedDoFetcher: HTTP results are RpcDisposableJsonResponse and websocket is Response & Disposable, so using res = await … type-checks when Workers RPC attaches disposers. If you pass only Pick<DurableObjectStub, "fetch"> (minimal mock), the return type is TypedHonoFetcher<Hono> with plain JsonResponse / Responsenot typed as Disposable, so we do not pretend mocks have RPC disposers. See Durable Object stubs and disposal and the RPC lifecycle doc.

honoDoFetcher<T>(stub)

Creates a typed fetcher for a Durable Object stub with support for both HTTP and WebSocket connections.

Returns: TypedDoFetcher<T> & Disposable when T is a full DurableObjectStub<DOWithHonoApp>; TypedHonoFetcher<Hono> & Disposable when T is only Pick<DurableObjectStub<DOWithHonoApp>, "fetch"> (e.g. unit tests).

using api = honoDoFetcher(env.MY_DO.getByName('example'));

// HTTP — dispose each RPC Response (stub disposal alone is not enough)
using res = await api.get({ url: '/status' });
const data = await res.json();

// WebSocket
using wsRes = await api.websocket({ url: '/ws' });
wsRes.webSocket?.accept();

honoDoFetcherWithName<T>(namespace, name)

Convenience method to create a fetcher from a namespace and name. Uses a single stub internally and wires disposal to that stub.

Returns: TypedDoFetcher<DurableObjectStub<T>> & Disposable

using api = honoDoFetcherWithName(env.MY_DO, 'example');

using res = await api.get({ url: '/status' });
await res.json();

using wsRes = await api.websocket({ url: '/chat' });
wsRes.webSocket?.accept();

honoDoFetcherWithId<T>(namespace, id)

Convenience method to create a fetcher from a namespace and hex ID string.

Returns: TypedDoFetcher<DurableObjectStub<T>> & Disposable

using api = honoDoFetcherWithId(env.MY_DO, 'abc123...');
using res = await api.get({ url: '/status' });
await res.json();

Durable Object stubs and disposal

Cloudflare Workers RPC gives separate disposers for the Durable Object stub and for non-primitive RPC results such as the Response from stub.fetch(). Disposing only the stub can still produce “An RPC stub was not disposed properly” if the Response was not released. See the official Workers RPC lifecycle documentation.

  • Stub (using api = honoDoFetcherWithName(...)): Releases the stub when the block ends. This does not release RPC Response objects from individual requests.
  • Response (HTTP / WebSocket upgrade): On a real stub (TypedDoFetcher), TypeScript types those results as Disposable so using res / using wsRes is valid. At runtime, [Symbol.dispose] is present when Workers RPC attaches it (often in production); minimal fetch-only mocks are typed as non-Disposable **Response**s because disposers are usually absent. Prefer using res when types allow it; otherwise call res[Symbol.dispose]() or use a DisposableStack as described in Cloudflare’s Workers RPC lifecycle documentation. Reading the body does not implicitly dispose the RPC result in this library.
  • Returning Response to the client: Do not use using res on a response you return from your Worker—disposal can run on scope exit before the runtime finishes serving it. Return the value directly; see Workers RPC lifecycle for pass-through patterns.
  • Vite SSR, some Miniflare setups, or test mocks may expose stubs or Response objects without Symbol.dispose; there is nothing to call in that case.
  • Errors from Symbol.dispose: If the runtime’s dispose implementation throws (for example during unwind after your code threw), the library catches the error and logs it with console.error. It does not rethrow, so your original error is not masked by a SuppressedError.
  • TypeScript using: Add "ESNext.Disposable" to the compilerOptions.lib array in your tsconfig.json (alongside your existing libs) so using and Disposable type-check. TypeScript 5.2+ is required for using. That applies to TypedDoFetcher (full DurableObjectStub path): RpcDisposableJsonResponse / Response & Disposable on websocket. Pick<stub, "fetch"> clients get ordinary TypedHonoFetcher return types without Disposable on responses. Cloudflare’s runtime rules for RPC disposal are documented under Workers RPC lifecycle.

Type Exports

TypedHonoFetcher<T>

The main fetcher type with methods for all available HTTP verbs.

import type { TypedHonoFetcher } from '@firtoz/hono-fetcher';

function createApi(): TypedHonoFetcher<typeof app> {
  return honoFetcher<typeof app>(app.request);
}

JsonResponse<T>

Extended Response type with properly typed json() method.

import type { JsonResponse } from '@firtoz/hono-fetcher';

const response: JsonResponse<{ id: string }> = await api.get({ url: '/user' });
const data = await response.json(); // Type: { id: string }

RpcDisposableJsonResponse<T> / BaseDisposableTypedHonoFetcher<T> / HonoDoFetcherStubInput

RpcDisposableJsonResponse<T> is JsonResponse<T> & Disposable. BaseDisposableTypedHonoFetcher<T> mirrors TypedHonoFetcher<T> but uses that for HTTP methods and Response & Disposable for websocket. TypedDoFetcher is BaseDisposableTypedHonoFetcher<Hono<…, DO schema>> — used only when honoDoFetcher is given a full DurableObjectStub, or when using honoDoFetcherWithName / honoDoFetcherWithId. HonoDoFetcherStubInput documents the DurableObjectStub | Pick<stub, "fetch"> union for honoDoFetcher. Background: Workers RPC lifecycle (official disposal / using / DisposableStack guidance).

WebSocketConfig

Configuration options for WebSocket connections.

import type { WebSocketConfig } from '@firtoz/hono-fetcher';

const config: WebSocketConfig = {
  autoAccept: false, // Default: true
};

await api.websocket({ url: '/ws', config });

Options:

  • autoAccept?: boolean - Whether to automatically call accept() on the WebSocket. Defaults to true for convenience. Set to false if you need manual control over when the WebSocket is accepted (e.g., when using with ZodWebSocketClient).

ParsePathParams<T>

Utility type to extract path parameters from a route string.

import type { ParsePathParams } from '@firtoz/hono-fetcher';

type Params = ParsePathParams<'/users/:id/posts/:postId'>;
// Type: { id: string; postId: string }

DOWithHonoApp

Type for Durable Objects that expose a Hono app.

import type { DOWithHonoApp } from '@firtoz/hono-fetcher';

export class MyDO extends DurableObject implements DOWithHonoApp {
  app = new Hono()
    .get('/status', (c) => c.json({ status: 'ok' }));
}

Advanced Usage

Sharing Types Between Frontend and Backend

// backend/app.ts
export const app = new Hono()
  .get('/users/:id', (c) => c.json({ id: c.req.param('id'), name: 'User' }))
  .post('/users', async (c) => {
    const body = await c.req.json<{ name: string }>();
    return c.json({ id: '1', ...body });
  });

export type AppType = typeof app;

// frontend/api.ts
import type { AppType } from '../backend/app';
import { honoFetcher } from '@firtoz/hono-fetcher';

export const api = honoFetcher<AppType>((url, init) => {
  return fetch(`https://api.example.com${url}`, init);
});

Error Handling

try {
  const response = await api.post({
    url: '/users',
    body: { name: 'John' }
  });

  if (!response.ok) {
    const error = await response.json();
    console.error('API error:', error);
    return;
  }

  const user = await response.json();
  console.log('Created user:', user);
} catch (error) {
  console.error('Network error:', error);
}

Middleware and Authentication

const createAuthenticatedFetcher = <T extends Hono>(token: string) => {
  return honoFetcher<T>((url, init) => {
    return fetch(`https://api.example.com${url}`, {
      ...init,
      headers: {
        ...init?.headers,
        'Authorization': `Bearer ${token}`
      }
    });
  });
};

const api = createAuthenticatedFetcher<typeof app>(userToken);

License

MIT

Contributing

See CONTRIBUTING.md for details on how to contribute to this package.