@firtoz/hono-fetcher 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,400 @@
1
+ # @firtoz/hono-fetcher
2
+
3
+ Type-safe Hono API client with full TypeScript inference for routes, params, and payloads.
4
+
5
+ ## Features
6
+
7
+ - 🔒 **Fully Type-Safe** - Complete TypeScript inference for routes, parameters, request bodies, and responses
8
+ - 🎯 **Path Parameters** - Automatic extraction and validation of path parameters (`:id`, `:slug`, etc.)
9
+ - 📝 **Request Bodies** - Type-safe JSON and form data support with automatic serialization
10
+ - 🌐 **Cloudflare Workers** - First-class support for Durable Objects with `honoDoFetcher`
11
+ - 🚀 **Zero Runtime Overhead** - All type inference happens at compile time
12
+ - 🔄 **Full HTTP Methods** - Support for GET, POST, PUT, DELETE, and PATCH
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ bun add @firtoz/hono-fetcher
18
+ ```
19
+
20
+ ### Peer Dependencies
21
+
22
+ This package requires the following peer dependencies:
23
+
24
+ ```bash
25
+ bun add hono
26
+ ```
27
+
28
+ For Durable Object support:
29
+
30
+ ```bash
31
+ bun add @cloudflare/workers-types
32
+ ```
33
+
34
+ ## Quick Start
35
+
36
+ ### Basic Usage
37
+
38
+ ```typescript
39
+ import { Hono } from 'hono';
40
+ import { honoFetcher } from '@firtoz/hono-fetcher';
41
+
42
+ // Define your Hono app
43
+ const app = new Hono()
44
+ .get('/users/:id', (c) => {
45
+ const id = c.req.param('id');
46
+ return c.json({ id, name: `User ${id}` });
47
+ })
48
+ .post('/users', async (c) => {
49
+ const body = await c.req.json<{ name: string }>();
50
+ return c.json({ id: '123', ...body });
51
+ });
52
+
53
+ // Create a typed fetcher
54
+ const api = honoFetcher<typeof app>(app.request);
55
+
56
+ // Use it with full type safety!
57
+ const response = await api.get({
58
+ url: '/users/:id',
59
+ params: { id: '123' }, // ✅ Type-safe params
60
+ });
61
+
62
+ const user = await response.json(); // ✅ Inferred type: { id: string; name: string }
63
+
64
+ // POST with body
65
+ await api.post({
66
+ url: '/users',
67
+ body: { name: 'John' }, // ✅ Type-safe body
68
+ });
69
+ ```
70
+
71
+ ### Remote API Usage
72
+
73
+ ```typescript
74
+ import { honoFetcher } from '@firtoz/hono-fetcher';
75
+
76
+ // For a remote API, you need to define the app type
77
+ // (Usually exported from your backend)
78
+ const api = honoFetcher<typeof app>((url, init) => {
79
+ return fetch(`https://api.example.com${url}`, init);
80
+ });
81
+ ```
82
+
83
+ ### Durable Objects
84
+
85
+ ```typescript
86
+ import { honoDoFetcher, honoDoFetcherWithName } from '@firtoz/hono-fetcher/honoDoFetcher';
87
+ import { DurableObject } from 'cloudflare:workers';
88
+ import { Hono } from 'hono';
89
+
90
+ // Define your Durable Object with a Hono app
91
+ export class ChatRoomDO extends DurableObject {
92
+ app = new Hono()
93
+ .get('/messages', (c) => {
94
+ return c.json({ messages: [] });
95
+ })
96
+ .post('/messages', async (c) => {
97
+ const { text } = await c.req.json<{ text: string }>();
98
+ return c.json({ id: '1', text });
99
+ });
100
+
101
+ fetch(request: Request) {
102
+ return this.app.fetch(request);
103
+ }
104
+ }
105
+
106
+ // In your worker
107
+ export default {
108
+ async fetch(request: Request, env: Env): Promise<Response> {
109
+ // Option 1: From a stub
110
+ const stub = env.CHAT_ROOM.get(env.CHAT_ROOM.idFromName('room-1'));
111
+ const api = honoDoFetcher(stub);
112
+
113
+ // Option 2: Directly with name
114
+ const api2 = honoDoFetcherWithName(env.CHAT_ROOM, 'room-1');
115
+
116
+ // Use it!
117
+ const response = await api.get({ url: '/messages' });
118
+ return response;
119
+ }
120
+ };
121
+ ```
122
+
123
+ ## API Reference
124
+
125
+ ### `honoFetcher<T>(fetcher)`
126
+
127
+ Creates a type-safe API client from a Hono app type.
128
+
129
+ #### Parameters
130
+
131
+ - `fetcher: (url: string, init?: RequestInit) => Response | Promise<Response>` - Function that performs the actual fetch
132
+
133
+ #### Returns
134
+
135
+ A typed fetcher with methods for each HTTP verb: `get`, `post`, `put`, `delete`, `patch`
136
+
137
+ #### Example
138
+
139
+ ```typescript
140
+ const api = honoFetcher<typeof app>(app.request);
141
+ ```
142
+
143
+ ### Method Signature
144
+
145
+ All methods follow this signature:
146
+
147
+ ```typescript
148
+ method({
149
+ url: string; // The route path
150
+ params?: object; // Path parameters (required if route has :params)
151
+ body?: object; // Request body (for POST/PUT/PATCH)
152
+ form?: object; // Form data (for POST/PUT/PATCH)
153
+ init?: RequestInit; // Additional fetch options
154
+ })
155
+ ```
156
+
157
+ ### Path Parameters
158
+
159
+ Routes with path parameters (`:id`, `:slug`, etc.) require the `params` field:
160
+
161
+ ```typescript
162
+ const app = new Hono()
163
+ .get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
164
+ .get('/posts/:id/comments/:commentId', (c) =>
165
+ c.json({
166
+ postId: c.req.param('id'),
167
+ commentId: c.req.param('commentId')
168
+ })
169
+ );
170
+
171
+ const api = honoFetcher<typeof app>(app.request);
172
+
173
+ // Single parameter
174
+ await api.get({
175
+ url: '/users/:id',
176
+ params: { id: '123' } // ✅ Required and type-safe
177
+ });
178
+
179
+ // Multiple parameters
180
+ await api.get({
181
+ url: '/posts/:id/comments/:commentId',
182
+ params: { id: '1', commentId: '42' } // ✅ Both required
183
+ });
184
+ ```
185
+
186
+ ### Request Bodies
187
+
188
+ #### JSON Bodies
189
+
190
+ ```typescript
191
+ const app = new Hono()
192
+ .post('/users', async (c) => {
193
+ const { name, email } = await c.req.json<{ name: string; email: string }>();
194
+ return c.json({ id: '1', name, email });
195
+ });
196
+
197
+ const api = honoFetcher<typeof app>(app.request);
198
+
199
+ await api.post({
200
+ url: '/users',
201
+ body: { name: 'John', email: 'john@example.com' } // ✅ Type-safe
202
+ });
203
+ ```
204
+
205
+ #### Form Data
206
+
207
+ ```typescript
208
+ import { zValidator } from '@hono/zod-validator';
209
+ import { z } from 'zod';
210
+
211
+ const app = new Hono()
212
+ .post('/upload',
213
+ zValidator('form', z.object({
214
+ title: z.string(),
215
+ count: z.coerce.number()
216
+ })),
217
+ async (c) => {
218
+ const data = c.req.valid('form');
219
+ return c.json({ success: true, data });
220
+ }
221
+ );
222
+
223
+ const api = honoFetcher<typeof app>(app.request);
224
+
225
+ await api.post({
226
+ url: '/upload',
227
+ form: { title: 'Hello', count: '5' } // ✅ Automatically sent as FormData
228
+ });
229
+ ```
230
+
231
+ ### Custom Headers and Options
232
+
233
+ Pass additional `fetch` options via the `init` parameter:
234
+
235
+ ```typescript
236
+ await api.get({
237
+ url: '/users/:id',
238
+ params: { id: '123' },
239
+ init: {
240
+ headers: {
241
+ 'Authorization': 'Bearer token',
242
+ 'X-Custom-Header': 'value'
243
+ },
244
+ signal: abortController.signal
245
+ }
246
+ });
247
+ ```
248
+
249
+ ## Durable Objects API
250
+
251
+ ### `honoDoFetcher<T>(stub)`
252
+
253
+ Creates a typed fetcher for a Durable Object stub.
254
+
255
+ ```typescript
256
+ const stub = env.MY_DO.get(env.MY_DO.idFromName('example'));
257
+ const api = honoDoFetcher(stub);
258
+
259
+ await api.get({ url: '/status' });
260
+ ```
261
+
262
+ ### `honoDoFetcherWithName<T>(namespace, name)`
263
+
264
+ Convenience method to create a fetcher from a namespace and name.
265
+
266
+ ```typescript
267
+ const api = honoDoFetcherWithName(env.MY_DO, 'example');
268
+ await api.get({ url: '/status' });
269
+ ```
270
+
271
+ ### `honoDoFetcherWithId<T>(namespace, id)`
272
+
273
+ Convenience method to create a fetcher from a namespace and hex ID string.
274
+
275
+ ```typescript
276
+ const api = honoDoFetcherWithId(env.MY_DO, 'abc123...');
277
+ await api.get({ url: '/status' });
278
+ ```
279
+
280
+ ## Type Exports
281
+
282
+ ### `TypedHonoFetcher<T>`
283
+
284
+ The main fetcher type with methods for all available HTTP verbs.
285
+
286
+ ```typescript
287
+ import type { TypedHonoFetcher } from '@firtoz/hono-fetcher';
288
+
289
+ function createApi(): TypedHonoFetcher<typeof app> {
290
+ return honoFetcher<typeof app>(app.request);
291
+ }
292
+ ```
293
+
294
+ ### `JsonResponse<T>`
295
+
296
+ Extended `Response` type with properly typed `json()` method.
297
+
298
+ ```typescript
299
+ import type { JsonResponse } from '@firtoz/hono-fetcher';
300
+
301
+ const response: JsonResponse<{ id: string }> = await api.get({ url: '/user' });
302
+ const data = await response.json(); // Type: { id: string }
303
+ ```
304
+
305
+ ### `ParsePathParams<T>`
306
+
307
+ Utility type to extract path parameters from a route string.
308
+
309
+ ```typescript
310
+ import type { ParsePathParams } from '@firtoz/hono-fetcher';
311
+
312
+ type Params = ParsePathParams<'/users/:id/posts/:postId'>;
313
+ // Type: { id: string; postId: string }
314
+ ```
315
+
316
+ ### `DOWithHonoApp`
317
+
318
+ Type for Durable Objects that expose a Hono app.
319
+
320
+ ```typescript
321
+ import type { DOWithHonoApp } from '@firtoz/hono-fetcher/honoDoFetcher';
322
+
323
+ export class MyDO extends DurableObject implements DOWithHonoApp {
324
+ app = new Hono()
325
+ .get('/status', (c) => c.json({ status: 'ok' }));
326
+ }
327
+ ```
328
+
329
+ ## Advanced Usage
330
+
331
+ ### Sharing Types Between Frontend and Backend
332
+
333
+ ```typescript
334
+ // backend/app.ts
335
+ export const app = new Hono()
336
+ .get('/users/:id', (c) => c.json({ id: c.req.param('id'), name: 'User' }))
337
+ .post('/users', async (c) => {
338
+ const body = await c.req.json<{ name: string }>();
339
+ return c.json({ id: '1', ...body });
340
+ });
341
+
342
+ export type AppType = typeof app;
343
+
344
+ // frontend/api.ts
345
+ import type { AppType } from '../backend/app';
346
+ import { honoFetcher } from '@firtoz/hono-fetcher';
347
+
348
+ export const api = honoFetcher<AppType>((url, init) => {
349
+ return fetch(`https://api.example.com${url}`, init);
350
+ });
351
+ ```
352
+
353
+ ### Error Handling
354
+
355
+ ```typescript
356
+ try {
357
+ const response = await api.post({
358
+ url: '/users',
359
+ body: { name: 'John' }
360
+ });
361
+
362
+ if (!response.ok) {
363
+ const error = await response.json();
364
+ console.error('API error:', error);
365
+ return;
366
+ }
367
+
368
+ const user = await response.json();
369
+ console.log('Created user:', user);
370
+ } catch (error) {
371
+ console.error('Network error:', error);
372
+ }
373
+ ```
374
+
375
+ ### Middleware and Authentication
376
+
377
+ ```typescript
378
+ const createAuthenticatedFetcher = <T extends Hono>(token: string) => {
379
+ return honoFetcher<T>((url, init) => {
380
+ return fetch(`https://api.example.com${url}`, {
381
+ ...init,
382
+ headers: {
383
+ ...init?.headers,
384
+ 'Authorization': `Bearer ${token}`
385
+ }
386
+ });
387
+ });
388
+ };
389
+
390
+ const api = createAuthenticatedFetcher<typeof app>(userToken);
391
+ ```
392
+
393
+ ## License
394
+
395
+ MIT
396
+
397
+ ## Contributing
398
+
399
+ See [CONTRIBUTING.md](../../CONTRIBUTING.md) for details on how to contribute to this package.
400
+
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@firtoz/hono-fetcher",
3
+ "version": "1.0.0",
4
+ "description": "Type-safe Hono API client with full TypeScript inference for routes, params, and payloads",
5
+ "main": "./src/index.ts",
6
+ "module": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.ts",
11
+ "import": "./src/index.ts",
12
+ "require": "./src/index.ts"
13
+ },
14
+ "./honoDoFetcher": {
15
+ "types": "./src/honoDoFetcher.ts",
16
+ "import": "./src/honoDoFetcher.ts",
17
+ "require": "./src/honoDoFetcher.ts"
18
+ }
19
+ },
20
+ "files": [
21
+ "src/**/*.ts",
22
+ "!src/**/*.test.ts",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "typecheck": "tsc --noEmit",
27
+ "lint": "biome lint src --write",
28
+ "format": "biome format src --write",
29
+ "test": "bun test",
30
+ "test:watch": "bun test --watch"
31
+ },
32
+ "keywords": [
33
+ "typescript",
34
+ "hono",
35
+ "api-client",
36
+ "type-safe",
37
+ "fetcher",
38
+ "http-client",
39
+ "durable-objects",
40
+ "cloudflare"
41
+ ],
42
+ "author": "Firtina Ozbalikchi <firtoz@github.com>",
43
+ "license": "MIT",
44
+ "homepage": "https://github.com/firtoz/fullstack-toolkit#readme",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/firtoz/fullstack-toolkit.git",
48
+ "directory": "packages/hono-fetcher"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/firtoz/fullstack-toolkit/issues"
52
+ },
53
+ "peerDependencies": {
54
+ "@cloudflare/workers-types": "^4.20251004.0",
55
+ "hono": "^4.9.9"
56
+ },
57
+ "engines": {
58
+ "node": ">=18.0.0"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public"
62
+ },
63
+ "devDependencies": {
64
+ "@hono/node-server": "^1.14.1",
65
+ "@hono/zod-validator": "^0.4.1",
66
+ "bun-types": "^1.2.23",
67
+ "zod": "^3.24.1"
68
+ }
69
+ }
@@ -0,0 +1,57 @@
1
+ import type { Hono, Schema } from "hono";
2
+ import type { ExtractSchema } from "hono/types";
3
+ import { honoFetcher, type TypedHonoFetcher } from "./honoFetcher";
4
+
5
+ const DUMMY_URL = "http://dummy-url";
6
+
7
+ export type DOWithHonoApp<S extends Schema = Schema> =
8
+ Rpc.DurableObjectBranded & {
9
+ // biome-ignore lint/suspicious/noExplicitAny: We need to be able to pass in any schema
10
+ app: Hono<any, S>;
11
+ };
12
+
13
+ export type DOSchemaMap<T extends DOWithHonoApp> = T extends DOWithHonoApp
14
+ ? ExtractSchema<T["app"]>
15
+ : never;
16
+
17
+ export type DOSchemaKeys<T extends DOWithHonoApp> = string &
18
+ keyof DOSchemaMap<T>;
19
+
20
+ export type DOStubSchema<T extends DurableObjectStub> =
21
+ T extends DurableObjectStub<infer S>
22
+ ? S extends DOWithHonoApp
23
+ ? ExtractSchema<S["app"]>
24
+ : never
25
+ : never;
26
+
27
+ export type TypedDoFetcher<T extends DurableObjectStub> = TypedHonoFetcher<
28
+ // biome-ignore lint/suspicious/noExplicitAny: Generic parameter needs flexibility
29
+ Hono<any, DOStubSchema<T>>
30
+ >;
31
+
32
+ export const honoDoFetcher = <const T extends DurableObjectStub<DOWithHonoApp>>(
33
+ durableObject: T,
34
+ ): TypedDoFetcher<T> => {
35
+ // biome-ignore lint/suspicious/noExplicitAny: Generic parameter needs flexibility
36
+ return honoFetcher<Hono<any, DOStubSchema<T>>>((url, init) => {
37
+ return durableObject.fetch(`${DUMMY_URL}${url}`, init);
38
+ });
39
+ };
40
+
41
+ export const honoDoFetcherWithName = <
42
+ const T extends Rpc.DurableObjectBranded & DOWithHonoApp,
43
+ >(
44
+ namespace: DurableObjectNamespace<T>,
45
+ name: string,
46
+ ): TypedDoFetcher<DurableObjectStub<T>> => {
47
+ return honoDoFetcher(namespace.get(namespace.idFromName(name)));
48
+ };
49
+
50
+ export const honoDoFetcherWithId = <
51
+ const T extends Rpc.DurableObjectBranded & DOWithHonoApp,
52
+ >(
53
+ namespace: DurableObjectNamespace<T>,
54
+ id: string,
55
+ ): TypedDoFetcher<DurableObjectStub<T>> => {
56
+ return honoDoFetcher(namespace.get(namespace.idFromString(id)));
57
+ };
@@ -0,0 +1,187 @@
1
+ import type { Hono } from "hono";
2
+ import type { ExtractSchema } from "hono/types";
3
+
4
+ export type ParsePathParams<T extends string> =
5
+ T extends `${infer _Start}/:${infer Param}/${infer Rest}`
6
+ ? { [K in Param | keyof ParsePathParams<`/${Rest}`>]: string }
7
+ : T extends `${infer _Start}/:${infer Param}`
8
+ ? { [K in Param]: string }
9
+ : never;
10
+
11
+ export type HttpMethod = "get" | "post" | "put" | "delete" | "patch";
12
+
13
+ export type HonoSchemaKeys<T extends Hono> = string & keyof ExtractSchema<T>;
14
+
15
+ type FilterKeysByMethod<
16
+ TApp extends ExtractSchema<unknown>,
17
+ TMethod extends HttpMethod,
18
+ > = {
19
+ [K in keyof TApp as TApp[K] extends { [key in `$${TMethod}`]: unknown }
20
+ ? K
21
+ : never]: TApp[K];
22
+ };
23
+
24
+ type HonoSchema<TApp extends Hono> = {
25
+ [M in HttpMethod]: FilterKeysByMethod<ExtractSchema<TApp>, M>;
26
+ };
27
+
28
+ export type JsonResponse<T> = Omit<Response, "json"> & {
29
+ json: () => Promise<T>;
30
+ };
31
+
32
+ type HasPathParams<T extends string> = T extends `${string}:${string}`
33
+ ? true
34
+ : false;
35
+
36
+ type FetcherParams<SchemaPath extends string> =
37
+ HasPathParams<SchemaPath> extends true
38
+ ? {
39
+ params: ParsePathParams<SchemaPath>;
40
+ init?: RequestInit;
41
+ }
42
+ : {
43
+ params?: never;
44
+ init?: RequestInit;
45
+ };
46
+
47
+ // biome-ignore lint/complexity/noBannedTypes: We need an empty object to remove the body and form keys from the request object
48
+ type EmptyObject = {};
49
+
50
+ type TypedMethodFetcher<T extends Hono, M extends HttpMethod> = <
51
+ SchemaPath extends string & keyof HonoSchema<T>[M],
52
+ >(
53
+ request: {
54
+ url: SchemaPath;
55
+ } & FetcherParams<SchemaPath> &
56
+ (M extends "get" | "delete" ? EmptyObject : BodyParams<T, M, SchemaPath>),
57
+ ) => Promise<SchemaOutput<T, M, SchemaPath>>;
58
+
59
+ type SchemaOutput<
60
+ T extends Hono,
61
+ M extends HttpMethod,
62
+ SchemaPath extends string & keyof HonoSchema<T>[M],
63
+ DollarM extends `$${M}` & keyof HonoSchema<T>[M][SchemaPath] = `$${M}` &
64
+ keyof HonoSchema<T>[M][SchemaPath],
65
+ > = "output" extends keyof HonoSchema<T>[M][SchemaPath][DollarM]
66
+ ? JsonResponse<HonoSchema<T>[M][SchemaPath][DollarM]["output"]>
67
+ : never;
68
+
69
+ type BodyParams<
70
+ TApp extends Hono,
71
+ TMethod extends HttpMethod,
72
+ SchemaPath extends string & keyof HonoSchema<TApp>[TMethod],
73
+ DollarMethod extends `$${TMethod}` &
74
+ keyof HonoSchema<TApp>[TMethod][SchemaPath] = `$${TMethod}` &
75
+ keyof HonoSchema<TApp>[TMethod][SchemaPath],
76
+ > = "input" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]
77
+ ? "json" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]
78
+ ? "form" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]
79
+ ?
80
+ | {
81
+ body: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["json"];
82
+ }
83
+ | {
84
+ form: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["form"];
85
+ }
86
+ : {
87
+ body: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["json"];
88
+ }
89
+ : "form" extends keyof HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]
90
+ ? {
91
+ form: HonoSchema<TApp>[TMethod][SchemaPath][DollarMethod]["input"]["form"];
92
+ }
93
+ : { body?: unknown } | { form?: unknown }
94
+ : EmptyObject;
95
+
96
+ type AvailableMethods<T extends Hono> = {
97
+ [M in HttpMethod]: keyof HonoSchema<T>[M] extends never ? never : M;
98
+ }[HttpMethod];
99
+
100
+ export type BaseTypedHonoFetcher<T extends Hono> = {
101
+ [M in AvailableMethods<T>]: TypedMethodFetcher<T, M>;
102
+ };
103
+
104
+ const createMethodFetcher = <T extends Hono, M extends HttpMethod>(
105
+ fetcher: (
106
+ request: string,
107
+ init?: RequestInit,
108
+ ) => ReturnType<T["request"]> | Promise<ReturnType<T["request"]>>,
109
+ method: M,
110
+ ): TypedMethodFetcher<T, M> => {
111
+ return (async (request) => {
112
+ let finalUrl: string = request.url;
113
+
114
+ const { init = {}, params } = request;
115
+
116
+ if (params && typeof params === "object") {
117
+ finalUrl = Object.entries(params).reduce((acc, [key, value]) => {
118
+ return acc.replace(`:${key}`, value as string);
119
+ }, finalUrl);
120
+ }
121
+
122
+ const requestAsOptionalFormBody = request as {
123
+ form?: unknown;
124
+ body?: unknown;
125
+ };
126
+
127
+ let body: BodyInit | undefined;
128
+ if (requestAsOptionalFormBody.form) {
129
+ const formData = new FormData();
130
+ for (const [key, value] of Object.entries(
131
+ requestAsOptionalFormBody.form,
132
+ )) {
133
+ formData.append(key, value as string);
134
+ }
135
+ body = formData;
136
+ } else if (requestAsOptionalFormBody.body) {
137
+ body = JSON.stringify(requestAsOptionalFormBody.body) as BodyInit;
138
+ }
139
+
140
+ // biome-ignore lint/suspicious/noExplicitAny: Different runtimes have incompatible HeadersInit types
141
+ const newHeaders = new Headers(init.headers as any);
142
+
143
+ if (body && !requestAsOptionalFormBody.form) {
144
+ newHeaders.set("Content-Type", "application/json");
145
+ }
146
+
147
+ try {
148
+ return await fetcher(finalUrl, {
149
+ method: method.toUpperCase(),
150
+ headers: newHeaders,
151
+ ...(body ? { body } : {}),
152
+ ...init,
153
+ });
154
+ } catch (error) {
155
+ console.error(`Error ${method}ing`, error);
156
+ throw new Error(`Failed to ${method} ${finalUrl}: ${error}`);
157
+ }
158
+ }) as TypedMethodFetcher<T, M>;
159
+ };
160
+
161
+ export type TypedHonoFetcher<T extends Hono> = BaseTypedHonoFetcher<T>;
162
+
163
+ export const honoFetcher = <T extends Hono>(
164
+ fetcher: (
165
+ request: string,
166
+ init?: RequestInit,
167
+ ) => ReturnType<T["request"]> | Promise<ReturnType<T["request"]>>,
168
+ ): TypedHonoFetcher<T> => {
169
+ const methods = ["get", "post", "put", "delete", "patch"] as const;
170
+
171
+ const result = methods.reduce(
172
+ (acc, method) => {
173
+ (
174
+ acc as TypedHonoFetcher<T> & {
175
+ [M in typeof method]: TypedMethodFetcher<T, M>;
176
+ }
177
+ )[method] = createMethodFetcher(fetcher, method) as TypedMethodFetcher<
178
+ T,
179
+ typeof method
180
+ >;
181
+ return acc;
182
+ },
183
+ {} as TypedHonoFetcher<T>,
184
+ );
185
+
186
+ return result;
187
+ };
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ export {
2
+ honoFetcher,
3
+ type BaseTypedHonoFetcher,
4
+ type HonoSchemaKeys,
5
+ type HttpMethod,
6
+ type JsonResponse,
7
+ type ParsePathParams,
8
+ type TypedHonoFetcher,
9
+ } from "./honoFetcher";
10
+ export {
11
+ honoDoFetcher,
12
+ honoDoFetcherWithId,
13
+ honoDoFetcherWithName,
14
+ type DOSchemaKeys,
15
+ type DOSchemaMap,
16
+ type DOStubSchema,
17
+ type DOWithHonoApp,
18
+ type TypedDoFetcher,
19
+ } from "./honoDoFetcher";