enlace 0.0.1-beta.2 → 0.0.1-beta.21
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 +668 -85
- package/dist/hook/index.d.mts +238 -0
- package/dist/hook/index.d.ts +238 -0
- package/dist/{next/hook → hook}/index.js +335 -169
- package/dist/hook/index.mjs +591 -0
- package/dist/index.d.mts +73 -70
- package/dist/index.d.ts +73 -70
- package/dist/index.js +57 -327
- package/dist/index.mjs +59 -327
- package/package.json +17 -12
- package/dist/next/hook/index.d.mts +0 -124
- package/dist/next/hook/index.d.ts +0 -124
- package/dist/next/hook/index.mjs +0 -444
- package/dist/next/index.d.mts +0 -74
- package/dist/next/index.d.ts +0 -74
- package/dist/next/index.js +0 -111
- package/dist/next/index.mjs +0 -95
package/README.md
CHANGED
|
@@ -11,20 +11,26 @@ npm install enlace
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import {
|
|
14
|
+
import { enlaceHookReact } from "enlace/hook";
|
|
15
|
+
import { Endpoint } from "enlace";
|
|
16
|
+
|
|
17
|
+
// Define your API error type
|
|
18
|
+
type ApiError = { message: string; code: number };
|
|
15
19
|
|
|
16
20
|
type ApiSchema = {
|
|
17
21
|
posts: {
|
|
18
|
-
$get:
|
|
19
|
-
$post: Endpoint<Post,
|
|
22
|
+
$get: Post[]; // Simple: just data type
|
|
23
|
+
$post: Endpoint<Post, CreatePost>; // Data + Body
|
|
24
|
+
$put: Endpoint<Post, UpdatePost, CustomError>; // Data + Body + Custom Error
|
|
20
25
|
_: {
|
|
21
|
-
$get:
|
|
22
|
-
$delete:
|
|
26
|
+
$get: Post; // Simple: just data type
|
|
27
|
+
$delete: void; // Simple: void response
|
|
23
28
|
};
|
|
24
29
|
};
|
|
25
30
|
};
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
// Pass global error type as second generic
|
|
33
|
+
const useAPI = enlaceHookReact<ApiSchema, ApiError>("https://api.example.com");
|
|
28
34
|
```
|
|
29
35
|
|
|
30
36
|
## Schema Conventions
|
|
@@ -33,13 +39,13 @@ Defining a schema is **recommended** for full type safety, but **optional**. You
|
|
|
33
39
|
|
|
34
40
|
```typescript
|
|
35
41
|
// Without schema (untyped, but still works!)
|
|
36
|
-
const useAPI =
|
|
37
|
-
const { data } = useAPI((api) => api.any.path.you.want
|
|
42
|
+
const useAPI = enlaceHookReact("https://api.example.com");
|
|
43
|
+
const { data } = useAPI((api) => api.any.path.you.want.$get());
|
|
38
44
|
```
|
|
39
45
|
|
|
40
46
|
```typescript
|
|
41
47
|
// With schema (recommended for type safety)
|
|
42
|
-
const useAPI =
|
|
48
|
+
const useAPI = enlaceHookReact<ApiSchema>("https://api.example.com");
|
|
43
49
|
```
|
|
44
50
|
|
|
45
51
|
### Schema Structure
|
|
@@ -50,40 +56,157 @@ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com");
|
|
|
50
56
|
```typescript
|
|
51
57
|
import { Endpoint } from "enlace";
|
|
52
58
|
|
|
59
|
+
type ApiError = { message: string };
|
|
60
|
+
|
|
53
61
|
type ApiSchema = {
|
|
54
62
|
users: {
|
|
55
|
-
$get:
|
|
56
|
-
$post: Endpoint<User>;
|
|
57
|
-
_: {
|
|
58
|
-
|
|
59
|
-
$
|
|
60
|
-
$
|
|
63
|
+
$get: User[]; // GET /users (simple)
|
|
64
|
+
$post: Endpoint<User, CreateUser>; // POST /users with body
|
|
65
|
+
_: {
|
|
66
|
+
// /users/:id
|
|
67
|
+
$get: User; // GET /users/:id (simple)
|
|
68
|
+
$put: Endpoint<User, UpdateUser>; // PUT /users/:id with body
|
|
69
|
+
$delete: void; // DELETE /users/:id (void response)
|
|
61
70
|
profile: {
|
|
62
|
-
$get:
|
|
71
|
+
$get: Profile; // GET /users/:id/profile (simple)
|
|
63
72
|
};
|
|
64
73
|
};
|
|
65
74
|
};
|
|
66
75
|
};
|
|
67
76
|
|
|
77
|
+
// Pass global error type - applies to all endpoints
|
|
78
|
+
const api = enlace<ApiSchema, ApiError>("https://api.example.com");
|
|
79
|
+
|
|
68
80
|
// Usage
|
|
69
|
-
api.users
|
|
70
|
-
api.users[123]
|
|
71
|
-
api.users[123].profile
|
|
81
|
+
api.users.$get(); // GET /users
|
|
82
|
+
api.users[123].$get(); // GET /users/123
|
|
83
|
+
api.users[123].profile.$get(); // GET /users/123/profile
|
|
72
84
|
```
|
|
73
85
|
|
|
74
|
-
### Endpoint
|
|
86
|
+
### Endpoint Types
|
|
87
|
+
|
|
88
|
+
The `Endpoint` type helpers let you define response data, request body, query params, formData, and error types.
|
|
89
|
+
|
|
90
|
+
#### `Endpoint<TData, TBody?, TError?>`
|
|
91
|
+
|
|
92
|
+
For endpoints with JSON body:
|
|
75
93
|
|
|
76
94
|
```typescript
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
95
|
+
import { Endpoint } from "enlace";
|
|
96
|
+
|
|
97
|
+
type ApiSchema = {
|
|
98
|
+
posts: {
|
|
99
|
+
$get: Post[]; // Direct type (simplest)
|
|
100
|
+
$post: Endpoint<Post, CreatePost>; // Data + Body
|
|
101
|
+
$put: Endpoint<Post, UpdatePost, ValidationError>; // Data + Body + Error
|
|
102
|
+
$delete: void; // void response
|
|
103
|
+
$patch: Endpoint<Post, never, NotFoundError>; // Custom error without body
|
|
104
|
+
};
|
|
81
105
|
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### `EndpointWithQuery<TData, TQuery, TError?>`
|
|
109
|
+
|
|
110
|
+
For endpoints with typed query parameters:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { EndpointWithQuery } from "enlace";
|
|
82
114
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
type ApiSchema = {
|
|
116
|
+
users: {
|
|
117
|
+
$get: EndpointWithQuery<
|
|
118
|
+
User[],
|
|
119
|
+
{ page: number; limit: number; search?: string }
|
|
120
|
+
>;
|
|
121
|
+
};
|
|
122
|
+
posts: {
|
|
123
|
+
$get: EndpointWithQuery<
|
|
124
|
+
Post[],
|
|
125
|
+
{ status: "draft" | "published" },
|
|
126
|
+
ApiError
|
|
127
|
+
>;
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Usage - query params are fully typed
|
|
132
|
+
const { data } = useAPI((api) =>
|
|
133
|
+
api.users.$get({ query: { page: 1, limit: 10 } })
|
|
134
|
+
);
|
|
135
|
+
// api.users.$get({ query: { foo: "bar" } }); // ✗ Error: 'foo' does not exist
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
#### `EndpointWithFormData<TData, TFormData, TError?>`
|
|
139
|
+
|
|
140
|
+
For file uploads (multipart/form-data):
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { EndpointWithFormData } from "enlace";
|
|
144
|
+
|
|
145
|
+
type ApiSchema = {
|
|
146
|
+
uploads: {
|
|
147
|
+
$post: EndpointWithFormData<Upload, { file: Blob | File; name: string }>;
|
|
148
|
+
};
|
|
149
|
+
avatars: {
|
|
150
|
+
$post: EndpointWithFormData<Avatar, { image: File }, UploadError>;
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// Usage - formData is automatically converted to FormData
|
|
155
|
+
const { trigger } = useAPI((api) => api.uploads.$post);
|
|
156
|
+
trigger({
|
|
157
|
+
formData: {
|
|
158
|
+
file: selectedFile, // File object
|
|
159
|
+
name: "document.pdf", // String - converted automatically
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
// → Sends as multipart/form-data
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**FormData conversion rules:**
|
|
166
|
+
|
|
167
|
+
| Type | Conversion |
|
|
168
|
+
| ------------------------------- | -------------------------------- |
|
|
169
|
+
| `File` / `Blob` | Appended directly |
|
|
170
|
+
| `string` / `number` / `boolean` | Converted to string |
|
|
171
|
+
| `object` (nested) | JSON stringified |
|
|
172
|
+
| `array` of primitives | Each item appended separately |
|
|
173
|
+
| `array` of files | Each file appended with same key |
|
|
174
|
+
|
|
175
|
+
#### `EndpointFull<T>`
|
|
176
|
+
|
|
177
|
+
Object-style for complex endpoints:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { EndpointFull } from "enlace";
|
|
181
|
+
|
|
182
|
+
type ApiSchema = {
|
|
183
|
+
products: {
|
|
184
|
+
$post: EndpointFull<{
|
|
185
|
+
data: Product;
|
|
186
|
+
body: CreateProduct;
|
|
187
|
+
query: { categoryId: string };
|
|
188
|
+
error: ValidationError;
|
|
189
|
+
}>;
|
|
190
|
+
};
|
|
191
|
+
files: {
|
|
192
|
+
$post: EndpointFull<{
|
|
193
|
+
data: FileUpload;
|
|
194
|
+
formData: { file: File; description: string };
|
|
195
|
+
query: { folder: string };
|
|
196
|
+
}>;
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**Global error type:**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
type ApiError = { message: string; code: number };
|
|
205
|
+
|
|
206
|
+
// Second generic sets default error type for all endpoints
|
|
207
|
+
const api = enlace<ApiSchema, ApiError>("https://api.example.com");
|
|
208
|
+
// const useAPI = enlaceHookReact<ApiSchema, ApiError>("...");
|
|
209
|
+
// const useAPI = enlaceHookNext<ApiSchema, ApiError>("...");
|
|
87
210
|
```
|
|
88
211
|
|
|
89
212
|
## React Hooks
|
|
@@ -94,12 +217,12 @@ For GET requests that fetch data automatically:
|
|
|
94
217
|
|
|
95
218
|
```typescript
|
|
96
219
|
function Posts({ page, limit }: { page: number; limit: number }) {
|
|
97
|
-
const { data, loading, error
|
|
98
|
-
api.posts
|
|
220
|
+
const { data, loading, error } = useAPI((api) =>
|
|
221
|
+
api.posts.$get({ query: { page, limit, published: true } })
|
|
99
222
|
);
|
|
100
223
|
|
|
101
224
|
if (loading) return <div>Loading...</div>;
|
|
102
|
-
if (
|
|
225
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
103
226
|
|
|
104
227
|
return (
|
|
105
228
|
<ul>
|
|
@@ -112,25 +235,152 @@ function Posts({ page, limit }: { page: number; limit: number }) {
|
|
|
112
235
|
```
|
|
113
236
|
|
|
114
237
|
**Features:**
|
|
238
|
+
|
|
115
239
|
- Auto-fetches on mount
|
|
116
240
|
- Re-fetches when dependencies change (no deps array needed!)
|
|
117
241
|
- Returns cached data while revalidating
|
|
242
|
+
- **Request deduplication** — identical requests from multiple components trigger only one fetch
|
|
243
|
+
|
|
244
|
+
### Conditional Fetching
|
|
245
|
+
|
|
246
|
+
Skip fetching with the `enabled` option:
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
function ProductForm({ id }: { id: string | "new" }) {
|
|
250
|
+
// Skip fetching when creating a new product
|
|
251
|
+
const { data, loading } = useAPI(
|
|
252
|
+
(api) => api.products[id].$get(),
|
|
253
|
+
{ enabled: id !== "new" }
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
if (id === "new") return <CreateProductForm />;
|
|
257
|
+
if (loading) return <div>Loading...</div>;
|
|
258
|
+
return <EditProductForm product={data} />;
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
// Also useful when waiting for a dependency
|
|
264
|
+
function UserPosts({ userId }: { userId: string | undefined }) {
|
|
265
|
+
const { data } = useAPI((api) => api.users[userId!].posts.$get(), {
|
|
266
|
+
enabled: userId !== undefined,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
```
|
|
118
270
|
|
|
119
271
|
```typescript
|
|
120
272
|
function Post({ id }: { id: number }) {
|
|
121
273
|
// Automatically re-fetches when `id` or query values change
|
|
122
|
-
const { data } = useAPI((api) => api.posts[id]
|
|
274
|
+
const { data } = useAPI((api) => api.posts[id].$get({ query: { include: "author" } }));
|
|
123
275
|
return <div>{data?.title}</div>;
|
|
124
276
|
}
|
|
125
277
|
```
|
|
126
278
|
|
|
279
|
+
### Polling
|
|
280
|
+
|
|
281
|
+
Automatically refetch data at intervals using the `pollingInterval` option. Polling uses sequential timing — the interval starts counting **after** the previous request completes, preventing request pile-up:
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
function Notifications() {
|
|
285
|
+
const { data } = useAPI(
|
|
286
|
+
(api) => api.notifications.$get(),
|
|
287
|
+
{ pollingInterval: 5000 } // Refetch every 5 seconds after previous request completes
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return <NotificationList notifications={data} />;
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
**Behavior:**
|
|
295
|
+
|
|
296
|
+
- Polling starts after the initial fetch completes
|
|
297
|
+
- Next poll is scheduled only after the current request finishes (success or error)
|
|
298
|
+
- Continues polling even on errors (retry behavior)
|
|
299
|
+
- Stops when component unmounts or `enabled` becomes `false`
|
|
300
|
+
- Resets when component remounts
|
|
301
|
+
|
|
302
|
+
**Dynamic polling with function:**
|
|
303
|
+
|
|
304
|
+
Use a function to conditionally poll based on the response data or error:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
function OrderStatus({ orderId }: { orderId: string }) {
|
|
308
|
+
const { data } = useAPI(
|
|
309
|
+
(api) => api.orders[orderId].$get(),
|
|
310
|
+
{
|
|
311
|
+
// Poll every 2s while pending, stop when completed
|
|
312
|
+
pollingInterval: (order) => order?.status === "pending" ? 2000 : false,
|
|
313
|
+
}
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
return <div>Status: {data?.status}</div>;
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The function receives `(data, error)` and should return:
|
|
321
|
+
|
|
322
|
+
- `number`: Interval in milliseconds
|
|
323
|
+
- `false`: Stop polling
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// Poll faster when there's an error (retry), slower otherwise
|
|
327
|
+
{
|
|
328
|
+
pollingInterval: (data, error) => (error ? 1000 : 10000);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Stop polling once data meets a condition
|
|
332
|
+
{
|
|
333
|
+
pollingInterval: (order) => (order?.status === "completed" ? false : 3000);
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Combined with conditional fetching:**
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
function OrderStatus({ orderId }: { orderId: string | undefined }) {
|
|
341
|
+
const { data } = useAPI((api) => api.orders[orderId!].$get(), {
|
|
342
|
+
enabled: !!orderId,
|
|
343
|
+
pollingInterval: 10000, // Poll every 10 seconds
|
|
344
|
+
});
|
|
345
|
+
// Polling only runs when orderId is defined
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### Request Deduplication
|
|
350
|
+
|
|
351
|
+
Multiple components requesting the same data will share a single network request:
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// Both components render at the same time
|
|
355
|
+
function PostTitle({ id }: { id: number }) {
|
|
356
|
+
const { data } = useAPI((api) => api.posts[id].$get());
|
|
357
|
+
return <h1>{data?.title}</h1>;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function PostBody({ id }: { id: number }) {
|
|
361
|
+
const { data } = useAPI((api) => api.posts[id].$get());
|
|
362
|
+
return <p>{data?.body}</p>;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Only ONE fetch request is made to GET /posts/123
|
|
366
|
+
// Both components share the same cached result
|
|
367
|
+
function PostPage() {
|
|
368
|
+
return (
|
|
369
|
+
<>
|
|
370
|
+
<PostTitle id={123} />
|
|
371
|
+
<PostBody id={123} />
|
|
372
|
+
</>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
127
377
|
### Selector Mode (Manual Trigger)
|
|
128
378
|
|
|
129
379
|
For mutations or lazy-loaded requests:
|
|
130
380
|
|
|
131
381
|
```typescript
|
|
132
382
|
function DeleteButton({ id }: { id: number }) {
|
|
133
|
-
const { trigger, loading } = useAPI((api) => api.posts[id]
|
|
383
|
+
const { trigger, loading } = useAPI((api) => api.posts[id].$delete);
|
|
134
384
|
|
|
135
385
|
return (
|
|
136
386
|
<button onClick={() => trigger()} disabled={loading}>
|
|
@@ -144,11 +394,11 @@ function DeleteButton({ id }: { id: number }) {
|
|
|
144
394
|
|
|
145
395
|
```typescript
|
|
146
396
|
function CreatePost() {
|
|
147
|
-
const { trigger, loading, data } = useAPI((api) => api.posts
|
|
397
|
+
const { trigger, loading, data } = useAPI((api) => api.posts.$post);
|
|
148
398
|
|
|
149
399
|
const handleSubmit = async (title: string) => {
|
|
150
400
|
const result = await trigger({ body: { title } });
|
|
151
|
-
if (result.
|
|
401
|
+
if (!result.error) {
|
|
152
402
|
console.log("Created:", result.data);
|
|
153
403
|
}
|
|
154
404
|
};
|
|
@@ -157,6 +407,58 @@ function CreatePost() {
|
|
|
157
407
|
}
|
|
158
408
|
```
|
|
159
409
|
|
|
410
|
+
### Dynamic Path Parameters
|
|
411
|
+
|
|
412
|
+
Use `:paramName` syntax for dynamic IDs passed at trigger time:
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
function PostList({ posts }: { posts: Post[] }) {
|
|
416
|
+
// Define once with :id placeholder
|
|
417
|
+
const { trigger, loading } = useAPI((api) => api.posts[":id"].$delete);
|
|
418
|
+
|
|
419
|
+
const handleDelete = (postId: number) => {
|
|
420
|
+
// Pass the actual ID when triggering
|
|
421
|
+
trigger({ params: { id: postId } });
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<ul>
|
|
426
|
+
{posts.map((post) => (
|
|
427
|
+
<li key={post.id}>
|
|
428
|
+
{post.title}
|
|
429
|
+
<button onClick={() => handleDelete(post.id)} disabled={loading}>
|
|
430
|
+
Delete
|
|
431
|
+
</button>
|
|
432
|
+
</li>
|
|
433
|
+
))}
|
|
434
|
+
</ul>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**Multiple path parameters:**
|
|
440
|
+
|
|
441
|
+
```typescript
|
|
442
|
+
const { trigger } = useAPI(
|
|
443
|
+
(api) => api.users[":userId"].posts[":postId"].$delete
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
trigger({ params: { userId: "1", postId: "42" } });
|
|
447
|
+
// → DELETE /users/1/posts/42
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
**With request body:**
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
const { trigger } = useAPI((api) => api.products[":id"].$patch);
|
|
454
|
+
|
|
455
|
+
trigger({
|
|
456
|
+
params: { id: "123" },
|
|
457
|
+
body: { name: "Updated Product" },
|
|
458
|
+
});
|
|
459
|
+
// → PATCH /products/123 with body
|
|
460
|
+
```
|
|
461
|
+
|
|
160
462
|
## Caching & Auto-Revalidation
|
|
161
463
|
|
|
162
464
|
### Automatic Cache Tags (Zero Config)
|
|
@@ -172,7 +474,7 @@ function CreatePost() {
|
|
|
172
474
|
**Mutations automatically revalidate matching tags:**
|
|
173
475
|
|
|
174
476
|
```typescript
|
|
175
|
-
const { trigger } = useAPI((api) => api.posts
|
|
477
|
+
const { trigger } = useAPI((api) => api.posts.$post);
|
|
176
478
|
|
|
177
479
|
// POST /posts automatically revalidates 'posts' tag
|
|
178
480
|
// All queries with 'posts' tag will refetch!
|
|
@@ -189,10 +491,10 @@ This means in most cases, **you don't need to specify any tags manually**. The c
|
|
|
189
491
|
|
|
190
492
|
```typescript
|
|
191
493
|
// Component A: fetches posts (cached with tag 'posts')
|
|
192
|
-
const { data } = useAPI((api) => api.posts
|
|
494
|
+
const { data } = useAPI((api) => api.posts.$get());
|
|
193
495
|
|
|
194
496
|
// Component B: creates a post
|
|
195
|
-
const { trigger } = useAPI((api) => api.posts
|
|
497
|
+
const { trigger } = useAPI((api) => api.posts.$post);
|
|
196
498
|
trigger({ body: { title: "New" } });
|
|
197
499
|
// → Automatically revalidates 'posts' tag
|
|
198
500
|
// → Component A refetches automatically!
|
|
@@ -203,9 +505,13 @@ trigger({ body: { title: "New" } });
|
|
|
203
505
|
Control how long cached data is considered fresh:
|
|
204
506
|
|
|
205
507
|
```typescript
|
|
206
|
-
const useAPI =
|
|
207
|
-
|
|
208
|
-
}
|
|
508
|
+
const useAPI = enlaceHookReact<ApiSchema>(
|
|
509
|
+
"https://api.example.com",
|
|
510
|
+
{},
|
|
511
|
+
{
|
|
512
|
+
staleTime: 5000, // 5 seconds
|
|
513
|
+
}
|
|
514
|
+
);
|
|
209
515
|
```
|
|
210
516
|
|
|
211
517
|
- `staleTime: 0` (default) — Always revalidate on mount
|
|
@@ -217,29 +523,81 @@ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com", {}, {
|
|
|
217
523
|
Override auto-generated tags when needed:
|
|
218
524
|
|
|
219
525
|
```typescript
|
|
220
|
-
// Custom cache tags
|
|
221
|
-
const { data } = useAPI((api) => api.posts
|
|
526
|
+
// Custom cache tags (replaces auto-generated)
|
|
527
|
+
const { data } = useAPI((api) => api.posts.$get({ tags: ["my-custom-tag"] }));
|
|
222
528
|
|
|
223
|
-
// Custom revalidation tags
|
|
529
|
+
// Custom revalidation tags (replaces auto-generated)
|
|
224
530
|
trigger({
|
|
225
531
|
body: { title: "New" },
|
|
226
|
-
revalidateTags: ["posts", "dashboard"],
|
|
532
|
+
revalidateTags: ["posts", "dashboard"],
|
|
227
533
|
});
|
|
228
534
|
```
|
|
229
535
|
|
|
230
|
-
###
|
|
536
|
+
### Extending Auto-Generated Tags
|
|
537
|
+
|
|
538
|
+
Use `additionalTags` and `additionalRevalidateTags` to **merge** with auto-generated tags instead of replacing them:
|
|
231
539
|
|
|
232
540
|
```typescript
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
541
|
+
// Extend cache tags (merges with auto-generated)
|
|
542
|
+
const { data } = useAPI((api) =>
|
|
543
|
+
api.posts.$get({ additionalTags: ["custom-tag"] })
|
|
544
|
+
);
|
|
545
|
+
// If autoGenerateTags produces ['posts'], final tags: ['posts', 'custom-tag']
|
|
546
|
+
|
|
547
|
+
// Extend revalidation tags (merges with auto-generated)
|
|
548
|
+
trigger({
|
|
549
|
+
body: { title: "New" },
|
|
550
|
+
additionalRevalidateTags: ["dashboard", "stats"],
|
|
236
551
|
});
|
|
552
|
+
// If autoRevalidateTags produces ['posts'], final tags: ['posts', 'dashboard', 'stats']
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Behavior:**
|
|
556
|
+
|
|
557
|
+
| Scenario | `tags` / `revalidateTags` | `additionalTags` / `additionalRevalidateTags` | Final Tags |
|
|
558
|
+
|----------|---------------------------|-----------------------------------------------|------------|
|
|
559
|
+
| Override | `['custom']` | - | `['custom']` |
|
|
560
|
+
| Extend auto | - | `['extra']` | `['posts', 'extra']` |
|
|
561
|
+
| Both | `['custom']` | `['extra']` | `['custom', 'extra']` |
|
|
562
|
+
| Neither | - | - | `['posts']` (auto) |
|
|
563
|
+
|
|
564
|
+
### Manual Tag Invalidation
|
|
565
|
+
|
|
566
|
+
Use `invalidateTags` to manually trigger cache invalidation and refetch queries:
|
|
567
|
+
|
|
568
|
+
```typescript
|
|
569
|
+
import { invalidateTags } from "enlace/hook";
|
|
570
|
+
|
|
571
|
+
// Invalidate all queries tagged with 'posts'
|
|
572
|
+
invalidateTags(["posts"]);
|
|
573
|
+
|
|
574
|
+
// Invalidate multiple tags
|
|
575
|
+
invalidateTags(["posts", "users"]);
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
This is useful when you need to refresh data outside of the normal mutation flow, such as:
|
|
579
|
+
|
|
580
|
+
- After receiving a WebSocket message
|
|
581
|
+
- After a background sync
|
|
582
|
+
- After external state changes
|
|
583
|
+
|
|
584
|
+
### Disable Auto-Revalidation
|
|
585
|
+
|
|
586
|
+
```typescript
|
|
587
|
+
const useAPI = enlaceHookReact<ApiSchema>(
|
|
588
|
+
"https://api.example.com",
|
|
589
|
+
{},
|
|
590
|
+
{
|
|
591
|
+
autoGenerateTags: false, // Disable auto tag generation
|
|
592
|
+
autoRevalidateTags: false, // Disable auto revalidation
|
|
593
|
+
}
|
|
594
|
+
);
|
|
237
595
|
```
|
|
238
596
|
|
|
239
597
|
## Hook Options
|
|
240
598
|
|
|
241
599
|
```typescript
|
|
242
|
-
const useAPI =
|
|
600
|
+
const useAPI = enlaceHookReact<ApiSchema>(
|
|
243
601
|
"https://api.example.com",
|
|
244
602
|
{
|
|
245
603
|
// Default fetch options
|
|
@@ -247,22 +605,122 @@ const useAPI = createEnlaceHook<ApiSchema>(
|
|
|
247
605
|
},
|
|
248
606
|
{
|
|
249
607
|
// Hook options
|
|
250
|
-
autoGenerateTags: true,
|
|
251
|
-
autoRevalidateTags: true,
|
|
252
|
-
staleTime: 0,
|
|
608
|
+
autoGenerateTags: true, // Auto-generate cache tags from URL
|
|
609
|
+
autoRevalidateTags: true, // Auto-revalidate after mutations
|
|
610
|
+
staleTime: 0, // Cache freshness duration (ms)
|
|
611
|
+
}
|
|
612
|
+
);
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
### Async Headers
|
|
616
|
+
|
|
617
|
+
Headers can be provided as a static value, sync function, or async function. This is useful when you need to fetch headers dynamically (e.g., auth tokens from async storage):
|
|
618
|
+
|
|
619
|
+
```typescript
|
|
620
|
+
// Static headers
|
|
621
|
+
const useAPI = enlaceHookReact<ApiSchema>("https://api.example.com", {
|
|
622
|
+
headers: { Authorization: "Bearer token" },
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
// Sync function
|
|
626
|
+
const useAPI = enlaceHookReact<ApiSchema>("https://api.example.com", {
|
|
627
|
+
headers: () => ({ Authorization: `Bearer ${getToken()}` }),
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// Async function
|
|
631
|
+
const useAPI = enlaceHookReact<ApiSchema>("https://api.example.com", {
|
|
632
|
+
headers: async () => {
|
|
633
|
+
const token = await getTokenFromStorage();
|
|
634
|
+
return { Authorization: `Bearer ${token}` };
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
This also works for per-request headers:
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
const { data } = useAPI((api) =>
|
|
643
|
+
api.posts.$get({
|
|
644
|
+
headers: async () => {
|
|
645
|
+
const token = await refreshToken();
|
|
646
|
+
return { Authorization: `Bearer ${token}` };
|
|
647
|
+
},
|
|
648
|
+
})
|
|
649
|
+
);
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
### Global Callbacks
|
|
653
|
+
|
|
654
|
+
You can set up global `onSuccess` and `onError` callbacks that are called for every request:
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
const useAPI = enlaceHookReact<ApiSchema>(
|
|
658
|
+
"https://api.example.com",
|
|
659
|
+
{
|
|
660
|
+
headers: { Authorization: "Bearer token" },
|
|
661
|
+
},
|
|
662
|
+
{
|
|
663
|
+
onSuccess: (payload) => {
|
|
664
|
+
console.log("Request succeeded:", payload.status, payload.data);
|
|
665
|
+
},
|
|
666
|
+
onError: (payload) => {
|
|
667
|
+
if (payload.status === 0) {
|
|
668
|
+
// Network error
|
|
669
|
+
console.error("Network error:", payload.error.message);
|
|
670
|
+
} else {
|
|
671
|
+
// HTTP error (4xx, 5xx)
|
|
672
|
+
console.error("HTTP error:", payload.status, payload.error);
|
|
673
|
+
}
|
|
674
|
+
},
|
|
253
675
|
}
|
|
254
676
|
);
|
|
255
677
|
```
|
|
256
678
|
|
|
679
|
+
**Callback Payloads:**
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
// onSuccess payload
|
|
683
|
+
type EnlaceCallbackPayload<T> = {
|
|
684
|
+
status: number;
|
|
685
|
+
data: T;
|
|
686
|
+
headers: Headers;
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// onError payload (HTTP error or network error)
|
|
690
|
+
type EnlaceErrorCallbackPayload<T> =
|
|
691
|
+
| { status: number; error: T; headers: Headers } // HTTP error
|
|
692
|
+
| { status: 0; error: Error; headers: null }; // Network error
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
**Use cases:**
|
|
696
|
+
|
|
697
|
+
- Global error logging/reporting
|
|
698
|
+
- Toast notifications for all API errors
|
|
699
|
+
- Authentication refresh on 401 errors
|
|
700
|
+
- Analytics tracking
|
|
701
|
+
|
|
257
702
|
## Return Types
|
|
258
703
|
|
|
259
704
|
### Query Mode
|
|
260
705
|
|
|
261
706
|
```typescript
|
|
707
|
+
// Basic usage
|
|
708
|
+
const result = useAPI((api) => api.posts.$get());
|
|
709
|
+
|
|
710
|
+
// With options
|
|
711
|
+
const result = useAPI((api) => api.posts.$get(), {
|
|
712
|
+
enabled: true, // Skip fetching when false
|
|
713
|
+
pollingInterval: 5000, // Refetch every 5s after previous request completes
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
// With dynamic polling
|
|
717
|
+
const result = useAPI((api) => api.orders[id].$get(), {
|
|
718
|
+
pollingInterval: (order) => (order?.status === "pending" ? 2000 : false),
|
|
719
|
+
});
|
|
720
|
+
|
|
262
721
|
type UseEnlaceQueryResult<TData, TError> = {
|
|
263
|
-
loading: boolean;
|
|
264
|
-
fetching: boolean;
|
|
265
|
-
ok: boolean | undefined;
|
|
722
|
+
loading: boolean; // No cached data and fetching
|
|
723
|
+
fetching: boolean; // Request in progress
|
|
266
724
|
data: TData | undefined;
|
|
267
725
|
error: TError | undefined;
|
|
268
726
|
};
|
|
@@ -272,32 +730,61 @@ type UseEnlaceQueryResult<TData, TError> = {
|
|
|
272
730
|
|
|
273
731
|
```typescript
|
|
274
732
|
type UseEnlaceSelectorResult<TMethod> = {
|
|
275
|
-
trigger: TMethod;
|
|
733
|
+
trigger: TMethod; // Function to trigger the request
|
|
276
734
|
loading: boolean;
|
|
277
735
|
fetching: boolean;
|
|
278
|
-
ok: boolean | undefined;
|
|
279
736
|
data: TData | undefined;
|
|
280
737
|
error: TError | undefined;
|
|
281
738
|
};
|
|
282
739
|
```
|
|
283
740
|
|
|
741
|
+
### Query Options
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
type UseEnlaceQueryOptions<TData, TError> = {
|
|
745
|
+
enabled?: boolean; // Skip fetching when false (default: true)
|
|
746
|
+
pollingInterval?: // Refetch interval after request completes
|
|
747
|
+
| number // Fixed interval in ms
|
|
748
|
+
| false // Disable polling
|
|
749
|
+
| ((data: TData | undefined, error: TError | undefined) => number | false); // Dynamic
|
|
750
|
+
};
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
### Request Options
|
|
754
|
+
|
|
755
|
+
```typescript
|
|
756
|
+
type RequestOptions = {
|
|
757
|
+
query?: TQuery; // Query parameters (typed when using EndpointWithQuery/EndpointFull)
|
|
758
|
+
body?: TBody; // Request body (JSON)
|
|
759
|
+
formData?: TFormData; // FormData fields (auto-converted, for file uploads)
|
|
760
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>); // Request headers
|
|
761
|
+
tags?: string[]; // Cache tags - replaces auto-generated (GET only)
|
|
762
|
+
additionalTags?: string[]; // Cache tags - merges with auto-generated (GET only)
|
|
763
|
+
revalidateTags?: string[]; // Revalidation tags - replaces auto-generated
|
|
764
|
+
additionalRevalidateTags?: string[]; // Revalidation tags - merges with auto-generated
|
|
765
|
+
params?: Record<string, string | number>; // Dynamic path parameters
|
|
766
|
+
};
|
|
767
|
+
```
|
|
768
|
+
|
|
284
769
|
---
|
|
285
770
|
|
|
286
771
|
## Next.js Integration
|
|
287
772
|
|
|
288
773
|
### Server Components
|
|
289
774
|
|
|
290
|
-
Use `
|
|
775
|
+
Use `enlaceNext` from `enlace` for server components:
|
|
291
776
|
|
|
292
777
|
```typescript
|
|
293
|
-
import {
|
|
778
|
+
import { enlaceNext } from "enlace";
|
|
779
|
+
|
|
780
|
+
type ApiError = { message: string };
|
|
294
781
|
|
|
295
|
-
const api =
|
|
782
|
+
const api = enlaceNext<ApiSchema, ApiError>("https://api.example.com", {}, {
|
|
296
783
|
autoGenerateTags: true,
|
|
297
784
|
});
|
|
298
785
|
|
|
299
786
|
export default async function Page() {
|
|
300
|
-
const { data } = await api.posts
|
|
787
|
+
const { data } = await api.posts.$get({
|
|
301
788
|
revalidate: 60, // ISR: revalidate every 60 seconds
|
|
302
789
|
});
|
|
303
790
|
|
|
@@ -307,14 +794,16 @@ export default async function Page() {
|
|
|
307
794
|
|
|
308
795
|
### Client Components
|
|
309
796
|
|
|
310
|
-
Use `
|
|
797
|
+
Use `enlaceHookNext` from `enlace/hook` for client components:
|
|
311
798
|
|
|
312
799
|
```typescript
|
|
313
800
|
"use client";
|
|
314
801
|
|
|
315
|
-
import {
|
|
802
|
+
import { enlaceHookNext } from "enlace/hook";
|
|
316
803
|
|
|
317
|
-
|
|
804
|
+
type ApiError = { message: string };
|
|
805
|
+
|
|
806
|
+
const useAPI = enlaceHookNext<ApiSchema, ApiError>("https://api.example.com");
|
|
318
807
|
```
|
|
319
808
|
|
|
320
809
|
### Server-Side Revalidation
|
|
@@ -339,39 +828,78 @@ export async function revalidateAction(tags: string[], paths: string[]) {
|
|
|
339
828
|
|
|
340
829
|
```typescript
|
|
341
830
|
// useAPI.ts
|
|
342
|
-
import {
|
|
831
|
+
import { enlaceHookNext } from "enlace/hook";
|
|
343
832
|
import { revalidateAction } from "./actions";
|
|
344
833
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
834
|
+
type ApiError = { message: string };
|
|
835
|
+
|
|
836
|
+
const useAPI = enlaceHookNext<ApiSchema, ApiError>(
|
|
837
|
+
"/api",
|
|
838
|
+
{},
|
|
839
|
+
{
|
|
840
|
+
serverRevalidator: revalidateAction,
|
|
841
|
+
}
|
|
842
|
+
);
|
|
348
843
|
```
|
|
349
844
|
|
|
350
845
|
**In components:**
|
|
351
846
|
|
|
352
847
|
```typescript
|
|
353
848
|
function CreatePost() {
|
|
354
|
-
const { trigger } = useAPI((api) => api.posts
|
|
849
|
+
const { trigger } = useAPI((api) => api.posts.$post);
|
|
355
850
|
|
|
356
851
|
const handleCreate = () => {
|
|
357
852
|
trigger({
|
|
358
853
|
body: { title: "New Post" },
|
|
359
|
-
revalidateTags: ["posts"],
|
|
360
|
-
revalidatePaths: ["/posts"],
|
|
854
|
+
revalidateTags: ["posts"], // Passed to serverRevalidator
|
|
855
|
+
revalidatePaths: ["/posts"], // Passed to serverRevalidator
|
|
361
856
|
});
|
|
362
857
|
};
|
|
363
858
|
}
|
|
364
859
|
```
|
|
365
860
|
|
|
861
|
+
### CSR-Heavy Projects
|
|
862
|
+
|
|
863
|
+
For projects that primarily use client-side rendering with minimal SSR, you can disable server-side revalidation by default:
|
|
864
|
+
|
|
865
|
+
```typescript
|
|
866
|
+
const useAPI = enlaceHookNext<ApiSchema, ApiError>(
|
|
867
|
+
"/api",
|
|
868
|
+
{},
|
|
869
|
+
{
|
|
870
|
+
serverRevalidator: revalidateAction,
|
|
871
|
+
skipServerRevalidation: true, // Disable server revalidation by default
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
// Mutations won't trigger server revalidation by default
|
|
876
|
+
await trigger({ body: { title: "New Post" } });
|
|
877
|
+
|
|
878
|
+
// Opt-in to server revalidation when needed
|
|
879
|
+
await trigger({ body: { title: "New Post" }, serverRevalidate: true });
|
|
880
|
+
```
|
|
881
|
+
|
|
882
|
+
### Per-Request Server Revalidation Control
|
|
883
|
+
|
|
884
|
+
Override the global setting for individual requests:
|
|
885
|
+
|
|
886
|
+
```typescript
|
|
887
|
+
// Skip server revalidation for this request
|
|
888
|
+
await trigger({ body: data, serverRevalidate: false });
|
|
889
|
+
|
|
890
|
+
// Force server revalidation for this request
|
|
891
|
+
await trigger({ body: data, serverRevalidate: true });
|
|
892
|
+
```
|
|
893
|
+
|
|
366
894
|
### Next.js Request Options
|
|
367
895
|
|
|
368
896
|
```typescript
|
|
369
|
-
api.posts
|
|
370
|
-
tags: ["posts"],
|
|
371
|
-
revalidate: 60,
|
|
897
|
+
api.posts.$get({
|
|
898
|
+
tags: ["posts"], // Next.js cache tags
|
|
899
|
+
revalidate: 60, // ISR revalidation (seconds)
|
|
372
900
|
revalidateTags: ["posts"], // Tags to invalidate after mutation
|
|
373
|
-
revalidatePaths: ["/"],
|
|
374
|
-
|
|
901
|
+
revalidatePaths: ["/"], // Paths to revalidate after mutation
|
|
902
|
+
serverRevalidate: true, // Control server-side revalidation per-request
|
|
375
903
|
});
|
|
376
904
|
```
|
|
377
905
|
|
|
@@ -381,37 +909,92 @@ Works with Next.js API routes:
|
|
|
381
909
|
|
|
382
910
|
```typescript
|
|
383
911
|
// Client component calling /api/posts
|
|
384
|
-
const useAPI =
|
|
912
|
+
const useAPI = enlaceHookNext<ApiSchema, ApiError>("/api");
|
|
385
913
|
```
|
|
386
914
|
|
|
387
915
|
---
|
|
388
916
|
|
|
389
917
|
## API Reference
|
|
390
918
|
|
|
391
|
-
### `
|
|
919
|
+
### `enlaceHookReact<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
|
|
392
920
|
|
|
393
921
|
Creates a React hook for making API calls.
|
|
394
922
|
|
|
395
|
-
|
|
923
|
+
### `enlaceHookNext<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
|
|
924
|
+
|
|
925
|
+
Creates a Next.js hook with server revalidation support.
|
|
926
|
+
|
|
927
|
+
### `enlace<TSchema, TDefaultError>(baseUrl, options?, callbacks?)`
|
|
928
|
+
|
|
929
|
+
Creates a typed API client (non-hook, for direct calls or server components).
|
|
930
|
+
|
|
931
|
+
### `enlaceNext<TSchema, TDefaultError>(baseUrl, options?, nextOptions?)`
|
|
932
|
+
|
|
933
|
+
Creates a Next.js typed API client with caching support.
|
|
934
|
+
|
|
935
|
+
**Generic Parameters:**
|
|
936
|
+
|
|
937
|
+
- `TSchema` — API schema type defining endpoints
|
|
938
|
+
- `TDefaultError` — Default error type for all endpoints (default: `unknown`)
|
|
939
|
+
|
|
940
|
+
**Function Parameters:**
|
|
941
|
+
|
|
396
942
|
- `baseUrl` — Base URL for requests
|
|
397
943
|
- `options` — Default fetch options (headers, cache, etc.)
|
|
398
|
-
- `hookOptions` —
|
|
944
|
+
- `hookOptions` / `callbacks` / `nextOptions` — Additional configuration
|
|
399
945
|
|
|
400
946
|
**Hook Options:**
|
|
947
|
+
|
|
401
948
|
```typescript
|
|
402
949
|
type EnlaceHookOptions = {
|
|
403
|
-
autoGenerateTags?: boolean;
|
|
404
|
-
autoRevalidateTags?: boolean;
|
|
405
|
-
staleTime?: number;
|
|
950
|
+
autoGenerateTags?: boolean; // default: true
|
|
951
|
+
autoRevalidateTags?: boolean; // default: true
|
|
952
|
+
staleTime?: number; // default: 0
|
|
953
|
+
onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
|
|
954
|
+
onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
|
|
406
955
|
};
|
|
407
956
|
```
|
|
408
957
|
|
|
409
958
|
### Re-exports from enlace-core
|
|
410
959
|
|
|
411
|
-
- `Endpoint` — Type helper for
|
|
960
|
+
- `Endpoint` — Type helper for endpoints with JSON body
|
|
961
|
+
- `EndpointWithQuery` — Type helper for endpoints with typed query params
|
|
962
|
+
- `EndpointWithFormData` — Type helper for file upload endpoints
|
|
963
|
+
- `EndpointFull` — Object-style type helper for complex endpoints
|
|
412
964
|
- `EnlaceResponse` — Response type
|
|
413
965
|
- `EnlaceOptions` — Fetch options type
|
|
414
966
|
|
|
967
|
+
## OpenAPI Generation
|
|
968
|
+
|
|
969
|
+
Generate OpenAPI 3.0 specs from your TypeScript schema using [`enlace-openapi`](../openapi/README.md):
|
|
970
|
+
|
|
971
|
+
```bash
|
|
972
|
+
npm install enlace-openapi
|
|
973
|
+
enlace-openapi --schema ./types/APISchema.ts --output ./openapi.json
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
## Framework Adapters
|
|
977
|
+
|
|
978
|
+
### Hono
|
|
979
|
+
|
|
980
|
+
Use [`enlace-hono`](../hono/README.md) to automatically generate Enlace schemas from your Hono app:
|
|
981
|
+
|
|
982
|
+
```typescript
|
|
983
|
+
import { Hono } from "hono";
|
|
984
|
+
import type { HonoToEnlace } from "enlace-hono";
|
|
985
|
+
|
|
986
|
+
const app = new Hono()
|
|
987
|
+
.basePath("/api")
|
|
988
|
+
.get("/posts", (c) => c.json([{ id: 1, title: "Hello" }]))
|
|
989
|
+
.get("/posts/:id", (c) => c.json({ id: c.req.param("id") }));
|
|
990
|
+
|
|
991
|
+
// Auto-generate schema from Hono types
|
|
992
|
+
type ApiSchema = HonoToEnlace<typeof app>;
|
|
993
|
+
|
|
994
|
+
// Use with Enlace
|
|
995
|
+
const client = enlace<ApiSchema["api"]>("http://localhost:3000/api");
|
|
996
|
+
```
|
|
997
|
+
|
|
415
998
|
## License
|
|
416
999
|
|
|
417
1000
|
MIT
|