enlace 0.0.0-alpha.4 → 0.0.1-beta.10
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/LICENSE +21 -0
- package/README.md +728 -0
- package/dist/hook/index.d.mts +173 -0
- package/dist/hook/index.d.ts +173 -0
- package/dist/hook/index.js +516 -0
- package/dist/hook/index.mjs +498 -0
- package/dist/index.d.mts +75 -113
- package/dist/index.d.ts +75 -113
- package/dist/index.js +55 -101
- package/dist/index.mjs +60 -91
- package/package.json +14 -28
- package/dist/next/index.d.mts +0 -121
- package/dist/next/index.d.ts +0 -121
- package/dist/next/index.js +0 -191
- package/dist/next/index.mjs +0 -158
package/README.md
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
# enlace
|
|
2
|
+
|
|
3
|
+
Type-safe API client with React hooks and Next.js integration.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install enlace
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { createEnlaceHookReact } from "enlace/hook";
|
|
15
|
+
import { Endpoint } from "enlace";
|
|
16
|
+
|
|
17
|
+
// Define your API error type
|
|
18
|
+
type ApiError = { message: string; code: number };
|
|
19
|
+
|
|
20
|
+
type ApiSchema = {
|
|
21
|
+
posts: {
|
|
22
|
+
$get: Post[]; // Simple: just data type
|
|
23
|
+
$post: Endpoint<Post, CreatePost>; // Data + Body
|
|
24
|
+
$put: Endpoint<Post, UpdatePost, CustomError>; // Data + Body + Custom Error
|
|
25
|
+
_: {
|
|
26
|
+
$get: Post; // Simple: just data type
|
|
27
|
+
$delete: void; // Simple: void response
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Pass global error type as second generic
|
|
33
|
+
const useAPI = createEnlaceHookReact<ApiSchema, ApiError>(
|
|
34
|
+
"https://api.example.com"
|
|
35
|
+
);
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Schema Conventions
|
|
39
|
+
|
|
40
|
+
Defining a schema is **recommended** for full type safety, but **optional**. You can go without types:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// Without schema (untyped, but still works!)
|
|
44
|
+
const useAPI = createEnlaceHookReact("https://api.example.com");
|
|
45
|
+
const { data } = useAPI((api) => api.any.path.you.want.get());
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
// With schema (recommended for type safety)
|
|
50
|
+
const useAPI = createEnlaceHookReact<ApiSchema>("https://api.example.com");
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Schema Structure
|
|
54
|
+
|
|
55
|
+
- `$get`, `$post`, `$put`, `$patch`, `$delete` — HTTP method endpoints
|
|
56
|
+
- `_` — Dynamic path segment (e.g., `/users/:id`)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { Endpoint } from "enlace";
|
|
60
|
+
|
|
61
|
+
type ApiError = { message: string };
|
|
62
|
+
|
|
63
|
+
type ApiSchema = {
|
|
64
|
+
users: {
|
|
65
|
+
$get: User[]; // GET /users (simple)
|
|
66
|
+
$post: Endpoint<User, CreateUser>; // POST /users with body
|
|
67
|
+
_: {
|
|
68
|
+
// /users/:id
|
|
69
|
+
$get: User; // GET /users/:id (simple)
|
|
70
|
+
$put: Endpoint<User, UpdateUser>; // PUT /users/:id with body
|
|
71
|
+
$delete: void; // DELETE /users/:id (void response)
|
|
72
|
+
profile: {
|
|
73
|
+
$get: Profile; // GET /users/:id/profile (simple)
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Pass global error type - applies to all endpoints
|
|
80
|
+
const api = createEnlace<ApiSchema, ApiError>("https://api.example.com");
|
|
81
|
+
|
|
82
|
+
// Usage
|
|
83
|
+
api.users.get(); // GET /users
|
|
84
|
+
api.users[123].get(); // GET /users/123
|
|
85
|
+
api.users[123].profile.get(); // GET /users/123/profile
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Endpoint Type
|
|
89
|
+
|
|
90
|
+
The `Endpoint` type helper lets you define response data, request body, and optionally override the error type:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Signature: Endpoint<TData, TBody?, TError?>
|
|
94
|
+
type Endpoint<TData, TBody = never, TError = never>;
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Three ways to define endpoints:**
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
type ApiSchema = {
|
|
101
|
+
posts: {
|
|
102
|
+
// 1. Direct type (simplest) - just the data type
|
|
103
|
+
// Error comes from global default
|
|
104
|
+
$get: Post[];
|
|
105
|
+
|
|
106
|
+
// 2. Endpoint with body - Endpoint<Data, Body>
|
|
107
|
+
// Error comes from global default
|
|
108
|
+
$post: Endpoint<Post, CreatePost>;
|
|
109
|
+
|
|
110
|
+
// 3. Endpoint with custom error - Endpoint<Data, Body, Error>
|
|
111
|
+
// Overrides global error type for this endpoint
|
|
112
|
+
$put: Endpoint<Post, UpdatePost, ValidationError>;
|
|
113
|
+
|
|
114
|
+
// void response - use void directly
|
|
115
|
+
$delete: void;
|
|
116
|
+
|
|
117
|
+
// Custom error without body - use `never` for body
|
|
118
|
+
$patch: Endpoint<Post, never, NotFoundError>;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Global error type:**
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
type ApiError = { message: string; code: number };
|
|
127
|
+
|
|
128
|
+
// Second generic sets default error type for all endpoints
|
|
129
|
+
const api = createEnlace<ApiSchema, ApiError>("https://api.example.com");
|
|
130
|
+
// const useAPI = createEnlaceHookReact<ApiSchema, ApiError>("...");
|
|
131
|
+
// const useAPI = createEnlaceHookNext<ApiSchema, ApiError>("...");
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## React Hooks
|
|
135
|
+
|
|
136
|
+
### Query Mode (Auto-Fetch)
|
|
137
|
+
|
|
138
|
+
For GET requests that fetch data automatically:
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
function Posts({ page, limit }: { page: number; limit: number }) {
|
|
142
|
+
const { data, loading, error } = useAPI((api) =>
|
|
143
|
+
api.posts.get({ query: { page, limit, published: true } })
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (loading) return <div>Loading...</div>;
|
|
147
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<ul>
|
|
151
|
+
{data.map((post) => (
|
|
152
|
+
<li key={post.id}>{post.title}</li>
|
|
153
|
+
))}
|
|
154
|
+
</ul>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Features:**
|
|
160
|
+
|
|
161
|
+
- Auto-fetches on mount
|
|
162
|
+
- Re-fetches when dependencies change (no deps array needed!)
|
|
163
|
+
- Returns cached data while revalidating
|
|
164
|
+
- **Request deduplication** — identical requests from multiple components trigger only one fetch
|
|
165
|
+
|
|
166
|
+
### Conditional Fetching
|
|
167
|
+
|
|
168
|
+
Skip fetching with the `enabled` option:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
function ProductForm({ id }: { id: string | "new" }) {
|
|
172
|
+
// Skip fetching when creating a new product
|
|
173
|
+
const { data, loading } = useAPI(
|
|
174
|
+
(api) => api.products[id].get(),
|
|
175
|
+
{ enabled: id !== "new" }
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (id === "new") return <CreateProductForm />;
|
|
179
|
+
if (loading) return <div>Loading...</div>;
|
|
180
|
+
return <EditProductForm product={data} />;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// Also useful when waiting for a dependency
|
|
186
|
+
function UserPosts({ userId }: { userId: string | undefined }) {
|
|
187
|
+
const { data } = useAPI((api) => api.users[userId!].posts.get(), {
|
|
188
|
+
enabled: userId !== undefined,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
function Post({ id }: { id: number }) {
|
|
195
|
+
// Automatically re-fetches when `id` or query values change
|
|
196
|
+
const { data } = useAPI((api) => api.posts[id].get({ query: { include: "author" } }));
|
|
197
|
+
return <div>{data?.title}</div>;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Request Deduplication
|
|
202
|
+
|
|
203
|
+
Multiple components requesting the same data will share a single network request:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
// Both components render at the same time
|
|
207
|
+
function PostTitle({ id }: { id: number }) {
|
|
208
|
+
const { data } = useAPI((api) => api.posts[id].get());
|
|
209
|
+
return <h1>{data?.title}</h1>;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function PostBody({ id }: { id: number }) {
|
|
213
|
+
const { data } = useAPI((api) => api.posts[id].get());
|
|
214
|
+
return <p>{data?.body}</p>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Only ONE fetch request is made to GET /posts/123
|
|
218
|
+
// Both components share the same cached result
|
|
219
|
+
function PostPage() {
|
|
220
|
+
return (
|
|
221
|
+
<>
|
|
222
|
+
<PostTitle id={123} />
|
|
223
|
+
<PostBody id={123} />
|
|
224
|
+
</>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Selector Mode (Manual Trigger)
|
|
230
|
+
|
|
231
|
+
For mutations or lazy-loaded requests:
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
function DeleteButton({ id }: { id: number }) {
|
|
235
|
+
const { trigger, loading } = useAPI((api) => api.posts[id].delete);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<button onClick={() => trigger()} disabled={loading}>
|
|
239
|
+
{loading ? "Deleting..." : "Delete"}
|
|
240
|
+
</button>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**With request body:**
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
function CreatePost() {
|
|
249
|
+
const { trigger, loading, data } = useAPI((api) => api.posts.post);
|
|
250
|
+
|
|
251
|
+
const handleSubmit = async (title: string) => {
|
|
252
|
+
const result = await trigger({ body: { title } });
|
|
253
|
+
if (!result.error) {
|
|
254
|
+
console.log("Created:", result.data);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
return <button onClick={() => handleSubmit("New Post")}>Create</button>;
|
|
259
|
+
}
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Dynamic Path Parameters
|
|
263
|
+
|
|
264
|
+
Use `:paramName` syntax for dynamic IDs passed at trigger time:
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
function PostList({ posts }: { posts: Post[] }) {
|
|
268
|
+
// Define once with :id placeholder
|
|
269
|
+
const { trigger, loading } = useAPI((api) => api.posts[":id"].delete);
|
|
270
|
+
|
|
271
|
+
const handleDelete = (postId: number) => {
|
|
272
|
+
// Pass the actual ID when triggering
|
|
273
|
+
trigger({ pathParams: { id: postId } });
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<ul>
|
|
278
|
+
{posts.map((post) => (
|
|
279
|
+
<li key={post.id}>
|
|
280
|
+
{post.title}
|
|
281
|
+
<button onClick={() => handleDelete(post.id)} disabled={loading}>
|
|
282
|
+
Delete
|
|
283
|
+
</button>
|
|
284
|
+
</li>
|
|
285
|
+
))}
|
|
286
|
+
</ul>
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Multiple path parameters:**
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
const { trigger } = useAPI(
|
|
295
|
+
(api) => api.users[":userId"].posts[":postId"].delete
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
trigger({ pathParams: { userId: "1", postId: "42" } });
|
|
299
|
+
// → DELETE /users/1/posts/42
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**With request body:**
|
|
303
|
+
|
|
304
|
+
```typescript
|
|
305
|
+
const { trigger } = useAPI((api) => api.products[":id"].patch);
|
|
306
|
+
|
|
307
|
+
trigger({
|
|
308
|
+
pathParams: { id: "123" },
|
|
309
|
+
body: { name: "Updated Product" },
|
|
310
|
+
});
|
|
311
|
+
// → PATCH /products/123 with body
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Caching & Auto-Revalidation
|
|
315
|
+
|
|
316
|
+
### Automatic Cache Tags (Zero Config)
|
|
317
|
+
|
|
318
|
+
**Tags are automatically generated from URL paths** — no manual configuration needed:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
// GET /posts → tags: ['posts']
|
|
322
|
+
// GET /posts/123 → tags: ['posts', 'posts/123']
|
|
323
|
+
// GET /users/5/posts → tags: ['users', 'users/5', 'users/5/posts']
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Mutations automatically revalidate matching tags:**
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
const { trigger } = useAPI((api) => api.posts.post);
|
|
330
|
+
|
|
331
|
+
// POST /posts automatically revalidates 'posts' tag
|
|
332
|
+
// All queries with 'posts' tag will refetch!
|
|
333
|
+
trigger({ body: { title: "New Post" } });
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
This means in most cases, **you don't need to specify any tags manually**. The cache just works.
|
|
337
|
+
|
|
338
|
+
### How It Works
|
|
339
|
+
|
|
340
|
+
1. **Queries** automatically cache with tags derived from the URL
|
|
341
|
+
2. **Mutations** automatically revalidate tags derived from the URL
|
|
342
|
+
3. All queries matching those tags refetch automatically
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// Component A: fetches posts (cached with tag 'posts')
|
|
346
|
+
const { data } = useAPI((api) => api.posts.get());
|
|
347
|
+
|
|
348
|
+
// Component B: creates a post
|
|
349
|
+
const { trigger } = useAPI((api) => api.posts.post);
|
|
350
|
+
trigger({ body: { title: "New" } });
|
|
351
|
+
// → Automatically revalidates 'posts' tag
|
|
352
|
+
// → Component A refetches automatically!
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Stale Time
|
|
356
|
+
|
|
357
|
+
Control how long cached data is considered fresh:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
const useAPI = createEnlaceHookReact<ApiSchema>(
|
|
361
|
+
"https://api.example.com",
|
|
362
|
+
{},
|
|
363
|
+
{
|
|
364
|
+
staleTime: 5000, // 5 seconds
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
- `staleTime: 0` (default) — Always revalidate on mount
|
|
370
|
+
- `staleTime: 5000` — Data is fresh for 5 seconds
|
|
371
|
+
- `staleTime: Infinity` — Never revalidate automatically
|
|
372
|
+
|
|
373
|
+
### Manual Tag Override (Optional)
|
|
374
|
+
|
|
375
|
+
Override auto-generated tags when needed:
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
// Custom cache tags
|
|
379
|
+
const { data } = useAPI((api) => api.posts.get({ tags: ["my-custom-tag"] }));
|
|
380
|
+
|
|
381
|
+
// Custom revalidation tags
|
|
382
|
+
trigger({
|
|
383
|
+
body: { title: "New" },
|
|
384
|
+
revalidateTags: ["posts", "dashboard"], // Override auto-generated
|
|
385
|
+
});
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Disable Auto-Revalidation
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
const useAPI = createEnlaceHookReact<ApiSchema>(
|
|
392
|
+
"https://api.example.com",
|
|
393
|
+
{},
|
|
394
|
+
{
|
|
395
|
+
autoGenerateTags: false, // Disable auto tag generation
|
|
396
|
+
autoRevalidateTags: false, // Disable auto revalidation
|
|
397
|
+
}
|
|
398
|
+
);
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
## Hook Options
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
const useAPI = createEnlaceHookReact<ApiSchema>(
|
|
405
|
+
"https://api.example.com",
|
|
406
|
+
{
|
|
407
|
+
// Default fetch options
|
|
408
|
+
headers: { Authorization: "Bearer token" },
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
// Hook options
|
|
412
|
+
autoGenerateTags: true, // Auto-generate cache tags from URL
|
|
413
|
+
autoRevalidateTags: true, // Auto-revalidate after mutations
|
|
414
|
+
staleTime: 0, // Cache freshness duration (ms)
|
|
415
|
+
}
|
|
416
|
+
);
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Async Headers
|
|
420
|
+
|
|
421
|
+
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):
|
|
422
|
+
|
|
423
|
+
```typescript
|
|
424
|
+
// Static headers
|
|
425
|
+
const useAPI = createEnlaceHookReact<ApiSchema>("https://api.example.com", {
|
|
426
|
+
headers: { Authorization: "Bearer token" },
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
// Sync function
|
|
430
|
+
const useAPI = createEnlaceHookReact<ApiSchema>("https://api.example.com", {
|
|
431
|
+
headers: () => ({ Authorization: `Bearer ${getToken()}` }),
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
// Async function
|
|
435
|
+
const useAPI = createEnlaceHookReact<ApiSchema>("https://api.example.com", {
|
|
436
|
+
headers: async () => {
|
|
437
|
+
const token = await getTokenFromStorage();
|
|
438
|
+
return { Authorization: `Bearer ${token}` };
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
This also works for per-request headers:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
const { data } = useAPI((api) =>
|
|
447
|
+
api.posts.get({
|
|
448
|
+
headers: async () => {
|
|
449
|
+
const token = await refreshToken();
|
|
450
|
+
return { Authorization: `Bearer ${token}` };
|
|
451
|
+
},
|
|
452
|
+
})
|
|
453
|
+
);
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Global Callbacks
|
|
457
|
+
|
|
458
|
+
You can set up global `onSuccess` and `onError` callbacks that are called for every request:
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
const useAPI = createEnlaceHookReact<ApiSchema>(
|
|
462
|
+
"https://api.example.com",
|
|
463
|
+
{
|
|
464
|
+
headers: { Authorization: "Bearer token" },
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
onSuccess: (payload) => {
|
|
468
|
+
console.log("Request succeeded:", payload.status, payload.data);
|
|
469
|
+
},
|
|
470
|
+
onError: (payload) => {
|
|
471
|
+
if (payload.status === 0) {
|
|
472
|
+
// Network error
|
|
473
|
+
console.error("Network error:", payload.error.message);
|
|
474
|
+
} else {
|
|
475
|
+
// HTTP error (4xx, 5xx)
|
|
476
|
+
console.error("HTTP error:", payload.status, payload.error);
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
}
|
|
480
|
+
);
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Callback Payloads:**
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
// onSuccess payload
|
|
487
|
+
type EnlaceCallbackPayload<T> = {
|
|
488
|
+
status: number;
|
|
489
|
+
data: T;
|
|
490
|
+
headers: Headers;
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
// onError payload (HTTP error or network error)
|
|
494
|
+
type EnlaceErrorCallbackPayload<T> =
|
|
495
|
+
| { status: number; error: T; headers: Headers } // HTTP error
|
|
496
|
+
| { status: 0; error: Error; headers: null }; // Network error
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Use cases:**
|
|
500
|
+
|
|
501
|
+
- Global error logging/reporting
|
|
502
|
+
- Toast notifications for all API errors
|
|
503
|
+
- Authentication refresh on 401 errors
|
|
504
|
+
- Analytics tracking
|
|
505
|
+
|
|
506
|
+
## Return Types
|
|
507
|
+
|
|
508
|
+
### Query Mode
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
// Basic usage
|
|
512
|
+
const result = useAPI((api) => api.posts.get());
|
|
513
|
+
|
|
514
|
+
// With options
|
|
515
|
+
const result = useAPI(
|
|
516
|
+
(api) => api.posts.get(),
|
|
517
|
+
{ enabled: true } // Skip fetching when false
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
type UseEnlaceQueryResult<TData, TError> = {
|
|
521
|
+
loading: boolean; // No cached data and fetching
|
|
522
|
+
fetching: boolean; // Request in progress
|
|
523
|
+
data: TData | undefined;
|
|
524
|
+
error: TError | undefined;
|
|
525
|
+
};
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### Selector Mode
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
type UseEnlaceSelectorResult<TMethod> = {
|
|
532
|
+
trigger: TMethod; // Function to trigger the request
|
|
533
|
+
loading: boolean;
|
|
534
|
+
fetching: boolean;
|
|
535
|
+
data: TData | undefined;
|
|
536
|
+
error: TError | undefined;
|
|
537
|
+
};
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Request Options
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
type RequestOptions = {
|
|
544
|
+
query?: Record<string, unknown>; // Query parameters
|
|
545
|
+
body?: TBody; // Request body
|
|
546
|
+
headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>); // Request headers
|
|
547
|
+
tags?: string[]; // Cache tags (GET only)
|
|
548
|
+
revalidateTags?: string[]; // Tags to invalidate after mutation
|
|
549
|
+
pathParams?: Record<string, string | number>; // Dynamic path parameters
|
|
550
|
+
};
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
---
|
|
554
|
+
|
|
555
|
+
## Next.js Integration
|
|
556
|
+
|
|
557
|
+
### Server Components
|
|
558
|
+
|
|
559
|
+
Use `createEnlaceNext` from `enlace` for server components:
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
import { createEnlaceNext } from "enlace";
|
|
563
|
+
|
|
564
|
+
type ApiError = { message: string };
|
|
565
|
+
|
|
566
|
+
const api = createEnlaceNext<ApiSchema, ApiError>("https://api.example.com", {}, {
|
|
567
|
+
autoGenerateTags: true,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
export default async function Page() {
|
|
571
|
+
const { data } = await api.posts.get({
|
|
572
|
+
revalidate: 60, // ISR: revalidate every 60 seconds
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
return <PostList posts={data} />;
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
### Client Components
|
|
580
|
+
|
|
581
|
+
Use `createEnlaceHookNext` from `enlace/hook` for client components:
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
"use client";
|
|
585
|
+
|
|
586
|
+
import { createEnlaceHookNext } from "enlace/hook";
|
|
587
|
+
|
|
588
|
+
type ApiError = { message: string };
|
|
589
|
+
|
|
590
|
+
const useAPI = createEnlaceHookNext<ApiSchema, ApiError>(
|
|
591
|
+
"https://api.example.com"
|
|
592
|
+
);
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### Server-Side Revalidation
|
|
596
|
+
|
|
597
|
+
Trigger Next.js cache revalidation after mutations:
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
// actions.ts
|
|
601
|
+
"use server";
|
|
602
|
+
|
|
603
|
+
import { revalidateTag, revalidatePath } from "next/cache";
|
|
604
|
+
|
|
605
|
+
export async function revalidateAction(tags: string[], paths: string[]) {
|
|
606
|
+
for (const tag of tags) {
|
|
607
|
+
revalidateTag(tag);
|
|
608
|
+
}
|
|
609
|
+
for (const path of paths) {
|
|
610
|
+
revalidatePath(path);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
```typescript
|
|
616
|
+
// useAPI.ts
|
|
617
|
+
import { createEnlaceHookNext } from "enlace/hook";
|
|
618
|
+
import { revalidateAction } from "./actions";
|
|
619
|
+
|
|
620
|
+
type ApiError = { message: string };
|
|
621
|
+
|
|
622
|
+
const useAPI = createEnlaceHookNext<ApiSchema, ApiError>(
|
|
623
|
+
"/api",
|
|
624
|
+
{},
|
|
625
|
+
{
|
|
626
|
+
revalidator: revalidateAction,
|
|
627
|
+
}
|
|
628
|
+
);
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**In components:**
|
|
632
|
+
|
|
633
|
+
```typescript
|
|
634
|
+
function CreatePost() {
|
|
635
|
+
const { trigger } = useAPI((api) => api.posts.post);
|
|
636
|
+
|
|
637
|
+
const handleCreate = () => {
|
|
638
|
+
trigger({
|
|
639
|
+
body: { title: "New Post" },
|
|
640
|
+
revalidateTags: ["posts"], // Passed to revalidator
|
|
641
|
+
revalidatePaths: ["/posts"], // Passed to revalidator
|
|
642
|
+
});
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### Next.js Request Options
|
|
648
|
+
|
|
649
|
+
```typescript
|
|
650
|
+
api.posts.get({
|
|
651
|
+
tags: ["posts"], // Next.js cache tags
|
|
652
|
+
revalidate: 60, // ISR revalidation (seconds)
|
|
653
|
+
revalidateTags: ["posts"], // Tags to invalidate after mutation
|
|
654
|
+
revalidatePaths: ["/"], // Paths to revalidate after mutation
|
|
655
|
+
skipRevalidator: false, // Skip server-side revalidation
|
|
656
|
+
});
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### Relative URLs
|
|
660
|
+
|
|
661
|
+
Works with Next.js API routes:
|
|
662
|
+
|
|
663
|
+
```typescript
|
|
664
|
+
// Client component calling /api/posts
|
|
665
|
+
const useAPI = createEnlaceHookNext<ApiSchema, ApiError>("/api");
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## API Reference
|
|
671
|
+
|
|
672
|
+
### `createEnlaceHookReact<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
|
|
673
|
+
|
|
674
|
+
Creates a React hook for making API calls.
|
|
675
|
+
|
|
676
|
+
### `createEnlaceHookNext<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
|
|
677
|
+
|
|
678
|
+
Creates a Next.js hook with server revalidation support.
|
|
679
|
+
|
|
680
|
+
### `createEnlace<TSchema, TDefaultError>(baseUrl, options?, callbacks?)`
|
|
681
|
+
|
|
682
|
+
Creates a typed API client (non-hook, for direct calls or server components).
|
|
683
|
+
|
|
684
|
+
### `createEnlaceNext<TSchema, TDefaultError>(baseUrl, options?, nextOptions?)`
|
|
685
|
+
|
|
686
|
+
Creates a Next.js typed API client with caching support.
|
|
687
|
+
|
|
688
|
+
**Generic Parameters:**
|
|
689
|
+
|
|
690
|
+
- `TSchema` — API schema type defining endpoints
|
|
691
|
+
- `TDefaultError` — Default error type for all endpoints (default: `unknown`)
|
|
692
|
+
|
|
693
|
+
**Function Parameters:**
|
|
694
|
+
|
|
695
|
+
- `baseUrl` — Base URL for requests
|
|
696
|
+
- `options` — Default fetch options (headers, cache, etc.)
|
|
697
|
+
- `hookOptions` / `callbacks` / `nextOptions` — Additional configuration
|
|
698
|
+
|
|
699
|
+
**Hook Options:**
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
type EnlaceHookOptions = {
|
|
703
|
+
autoGenerateTags?: boolean; // default: true
|
|
704
|
+
autoRevalidateTags?: boolean; // default: true
|
|
705
|
+
staleTime?: number; // default: 0
|
|
706
|
+
onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
|
|
707
|
+
onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
|
|
708
|
+
};
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### Re-exports from enlace-core
|
|
712
|
+
|
|
713
|
+
- `Endpoint` — Type helper for schema definition
|
|
714
|
+
- `EnlaceResponse` — Response type
|
|
715
|
+
- `EnlaceOptions` — Fetch options type
|
|
716
|
+
|
|
717
|
+
## OpenAPI Generation
|
|
718
|
+
|
|
719
|
+
Generate OpenAPI 3.0 specs from your TypeScript schema using [`enlace-openapi`](../openapi/README.md):
|
|
720
|
+
|
|
721
|
+
```bash
|
|
722
|
+
npm install enlace-openapi
|
|
723
|
+
enlace-openapi --schema ./types/APISchema.ts --output ./openapi.json
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
## License
|
|
727
|
+
|
|
728
|
+
MIT
|