enlace 0.0.1-beta.1 → 0.0.1-beta.11

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,836 @@
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 Types
89
+
90
+ The `Endpoint` type helpers let you define response data, request body, query params, formData, and error types.
91
+
92
+ #### `Endpoint<TData, TBody?, TError?>`
93
+
94
+ For endpoints with JSON body:
95
+
96
+ ```typescript
97
+ import { Endpoint } from "enlace";
98
+
99
+ type ApiSchema = {
100
+ posts: {
101
+ $get: Post[]; // Direct type (simplest)
102
+ $post: Endpoint<Post, CreatePost>; // Data + Body
103
+ $put: Endpoint<Post, UpdatePost, ValidationError>; // Data + Body + Error
104
+ $delete: void; // void response
105
+ $patch: Endpoint<Post, never, NotFoundError>; // Custom error without body
106
+ };
107
+ };
108
+ ```
109
+
110
+ #### `EndpointWithQuery<TData, TQuery, TError?>`
111
+
112
+ For endpoints with typed query parameters:
113
+
114
+ ```typescript
115
+ import { EndpointWithQuery } from "enlace";
116
+
117
+ type ApiSchema = {
118
+ users: {
119
+ $get: EndpointWithQuery<User[], { page: number; limit: number; search?: string }>;
120
+ };
121
+ posts: {
122
+ $get: EndpointWithQuery<Post[], { status: "draft" | "published" }, ApiError>;
123
+ };
124
+ };
125
+
126
+ // Usage - query params are fully typed
127
+ const { data } = useAPI((api) => api.users.get({ query: { page: 1, limit: 10 } }));
128
+ // api.users.get({ query: { foo: "bar" } }); // ✗ Error: 'foo' does not exist
129
+ ```
130
+
131
+ #### `EndpointWithFormData<TData, TFormData, TError?>`
132
+
133
+ For file uploads (multipart/form-data):
134
+
135
+ ```typescript
136
+ import { EndpointWithFormData } from "enlace";
137
+
138
+ type ApiSchema = {
139
+ uploads: {
140
+ $post: EndpointWithFormData<Upload, { file: Blob | File; name: string }>;
141
+ };
142
+ avatars: {
143
+ $post: EndpointWithFormData<Avatar, { image: File }, UploadError>;
144
+ };
145
+ };
146
+
147
+ // Usage - formData is automatically converted to FormData
148
+ const { trigger } = useAPI((api) => api.uploads.post);
149
+ trigger({
150
+ formData: {
151
+ file: selectedFile, // File object
152
+ name: "document.pdf", // String - converted automatically
153
+ }
154
+ });
155
+ // → Sends as multipart/form-data
156
+ ```
157
+
158
+ **FormData conversion rules:**
159
+
160
+ | Type | Conversion |
161
+ |------|------------|
162
+ | `File` / `Blob` | Appended directly |
163
+ | `string` / `number` / `boolean` | Converted to string |
164
+ | `object` (nested) | JSON stringified |
165
+ | `array` of primitives | Each item appended separately |
166
+ | `array` of files | Each file appended with same key |
167
+
168
+ #### `EndpointFull<T>`
169
+
170
+ Object-style for complex endpoints:
171
+
172
+ ```typescript
173
+ import { EndpointFull } from "enlace";
174
+
175
+ type ApiSchema = {
176
+ products: {
177
+ $post: EndpointFull<{
178
+ data: Product;
179
+ body: CreateProduct;
180
+ query: { categoryId: string };
181
+ error: ValidationError;
182
+ }>;
183
+ };
184
+ files: {
185
+ $post: EndpointFull<{
186
+ data: FileUpload;
187
+ formData: { file: File; description: string };
188
+ query: { folder: string };
189
+ }>;
190
+ };
191
+ };
192
+ ```
193
+
194
+ **Global error type:**
195
+
196
+ ```typescript
197
+ type ApiError = { message: string; code: number };
198
+
199
+ // Second generic sets default error type for all endpoints
200
+ const api = createEnlace<ApiSchema, ApiError>("https://api.example.com");
201
+ // const useAPI = createEnlaceHookReact<ApiSchema, ApiError>("...");
202
+ // const useAPI = createEnlaceHookNext<ApiSchema, ApiError>("...");
203
+ ```
204
+
205
+ ## React Hooks
206
+
207
+ ### Query Mode (Auto-Fetch)
208
+
209
+ For GET requests that fetch data automatically:
210
+
211
+ ```typescript
212
+ function Posts({ page, limit }: { page: number; limit: number }) {
213
+ const { data, loading, error } = useAPI((api) =>
214
+ api.posts.get({ query: { page, limit, published: true } })
215
+ );
216
+
217
+ if (loading) return <div>Loading...</div>;
218
+ if (error) return <div>Error: {error.message}</div>;
219
+
220
+ return (
221
+ <ul>
222
+ {data.map((post) => (
223
+ <li key={post.id}>{post.title}</li>
224
+ ))}
225
+ </ul>
226
+ );
227
+ }
228
+ ```
229
+
230
+ **Features:**
231
+
232
+ - Auto-fetches on mount
233
+ - Re-fetches when dependencies change (no deps array needed!)
234
+ - Returns cached data while revalidating
235
+ - **Request deduplication** — identical requests from multiple components trigger only one fetch
236
+
237
+ ### Conditional Fetching
238
+
239
+ Skip fetching with the `enabled` option:
240
+
241
+ ```typescript
242
+ function ProductForm({ id }: { id: string | "new" }) {
243
+ // Skip fetching when creating a new product
244
+ const { data, loading } = useAPI(
245
+ (api) => api.products[id].get(),
246
+ { enabled: id !== "new" }
247
+ );
248
+
249
+ if (id === "new") return <CreateProductForm />;
250
+ if (loading) return <div>Loading...</div>;
251
+ return <EditProductForm product={data} />;
252
+ }
253
+ ```
254
+
255
+ ```typescript
256
+ // Also useful when waiting for a dependency
257
+ function UserPosts({ userId }: { userId: string | undefined }) {
258
+ const { data } = useAPI((api) => api.users[userId!].posts.get(), {
259
+ enabled: userId !== undefined,
260
+ });
261
+ }
262
+ ```
263
+
264
+ ```typescript
265
+ function Post({ id }: { id: number }) {
266
+ // Automatically re-fetches when `id` or query values change
267
+ const { data } = useAPI((api) => api.posts[id].get({ query: { include: "author" } }));
268
+ return <div>{data?.title}</div>;
269
+ }
270
+ ```
271
+
272
+ ### Request Deduplication
273
+
274
+ Multiple components requesting the same data will share a single network request:
275
+
276
+ ```typescript
277
+ // Both components render at the same time
278
+ function PostTitle({ id }: { id: number }) {
279
+ const { data } = useAPI((api) => api.posts[id].get());
280
+ return <h1>{data?.title}</h1>;
281
+ }
282
+
283
+ function PostBody({ id }: { id: number }) {
284
+ const { data } = useAPI((api) => api.posts[id].get());
285
+ return <p>{data?.body}</p>;
286
+ }
287
+
288
+ // Only ONE fetch request is made to GET /posts/123
289
+ // Both components share the same cached result
290
+ function PostPage() {
291
+ return (
292
+ <>
293
+ <PostTitle id={123} />
294
+ <PostBody id={123} />
295
+ </>
296
+ );
297
+ }
298
+ ```
299
+
300
+ ### Selector Mode (Manual Trigger)
301
+
302
+ For mutations or lazy-loaded requests:
303
+
304
+ ```typescript
305
+ function DeleteButton({ id }: { id: number }) {
306
+ const { trigger, loading } = useAPI((api) => api.posts[id].delete);
307
+
308
+ return (
309
+ <button onClick={() => trigger()} disabled={loading}>
310
+ {loading ? "Deleting..." : "Delete"}
311
+ </button>
312
+ );
313
+ }
314
+ ```
315
+
316
+ **With request body:**
317
+
318
+ ```typescript
319
+ function CreatePost() {
320
+ const { trigger, loading, data } = useAPI((api) => api.posts.post);
321
+
322
+ const handleSubmit = async (title: string) => {
323
+ const result = await trigger({ body: { title } });
324
+ if (!result.error) {
325
+ console.log("Created:", result.data);
326
+ }
327
+ };
328
+
329
+ return <button onClick={() => handleSubmit("New Post")}>Create</button>;
330
+ }
331
+ ```
332
+
333
+ ### Dynamic Path Parameters
334
+
335
+ Use `:paramName` syntax for dynamic IDs passed at trigger time:
336
+
337
+ ```typescript
338
+ function PostList({ posts }: { posts: Post[] }) {
339
+ // Define once with :id placeholder
340
+ const { trigger, loading } = useAPI((api) => api.posts[":id"].delete);
341
+
342
+ const handleDelete = (postId: number) => {
343
+ // Pass the actual ID when triggering
344
+ trigger({ pathParams: { id: postId } });
345
+ };
346
+
347
+ return (
348
+ <ul>
349
+ {posts.map((post) => (
350
+ <li key={post.id}>
351
+ {post.title}
352
+ <button onClick={() => handleDelete(post.id)} disabled={loading}>
353
+ Delete
354
+ </button>
355
+ </li>
356
+ ))}
357
+ </ul>
358
+ );
359
+ }
360
+ ```
361
+
362
+ **Multiple path parameters:**
363
+
364
+ ```typescript
365
+ const { trigger } = useAPI(
366
+ (api) => api.users[":userId"].posts[":postId"].delete
367
+ );
368
+
369
+ trigger({ pathParams: { userId: "1", postId: "42" } });
370
+ // → DELETE /users/1/posts/42
371
+ ```
372
+
373
+ **With request body:**
374
+
375
+ ```typescript
376
+ const { trigger } = useAPI((api) => api.products[":id"].patch);
377
+
378
+ trigger({
379
+ pathParams: { id: "123" },
380
+ body: { name: "Updated Product" },
381
+ });
382
+ // → PATCH /products/123 with body
383
+ ```
384
+
385
+ ## Caching & Auto-Revalidation
386
+
387
+ ### Automatic Cache Tags (Zero Config)
388
+
389
+ **Tags are automatically generated from URL paths** — no manual configuration needed:
390
+
391
+ ```typescript
392
+ // GET /posts → tags: ['posts']
393
+ // GET /posts/123 → tags: ['posts', 'posts/123']
394
+ // GET /users/5/posts → tags: ['users', 'users/5', 'users/5/posts']
395
+ ```
396
+
397
+ **Mutations automatically revalidate matching tags:**
398
+
399
+ ```typescript
400
+ const { trigger } = useAPI((api) => api.posts.post);
401
+
402
+ // POST /posts automatically revalidates 'posts' tag
403
+ // All queries with 'posts' tag will refetch!
404
+ trigger({ body: { title: "New Post" } });
405
+ ```
406
+
407
+ This means in most cases, **you don't need to specify any tags manually**. The cache just works.
408
+
409
+ ### How It Works
410
+
411
+ 1. **Queries** automatically cache with tags derived from the URL
412
+ 2. **Mutations** automatically revalidate tags derived from the URL
413
+ 3. All queries matching those tags refetch automatically
414
+
415
+ ```typescript
416
+ // Component A: fetches posts (cached with tag 'posts')
417
+ const { data } = useAPI((api) => api.posts.get());
418
+
419
+ // Component B: creates a post
420
+ const { trigger } = useAPI((api) => api.posts.post);
421
+ trigger({ body: { title: "New" } });
422
+ // → Automatically revalidates 'posts' tag
423
+ // → Component A refetches automatically!
424
+ ```
425
+
426
+ ### Stale Time
427
+
428
+ Control how long cached data is considered fresh:
429
+
430
+ ```typescript
431
+ const useAPI = createEnlaceHookReact<ApiSchema>(
432
+ "https://api.example.com",
433
+ {},
434
+ {
435
+ staleTime: 5000, // 5 seconds
436
+ }
437
+ );
438
+ ```
439
+
440
+ - `staleTime: 0` (default) — Always revalidate on mount
441
+ - `staleTime: 5000` — Data is fresh for 5 seconds
442
+ - `staleTime: Infinity` — Never revalidate automatically
443
+
444
+ ### Manual Tag Override (Optional)
445
+
446
+ Override auto-generated tags when needed:
447
+
448
+ ```typescript
449
+ // Custom cache tags
450
+ const { data } = useAPI((api) => api.posts.get({ tags: ["my-custom-tag"] }));
451
+
452
+ // Custom revalidation tags
453
+ trigger({
454
+ body: { title: "New" },
455
+ revalidateTags: ["posts", "dashboard"], // Override auto-generated
456
+ });
457
+ ```
458
+
459
+ ### Disable Auto-Revalidation
460
+
461
+ ```typescript
462
+ const useAPI = createEnlaceHookReact<ApiSchema>(
463
+ "https://api.example.com",
464
+ {},
465
+ {
466
+ autoGenerateTags: false, // Disable auto tag generation
467
+ autoRevalidateTags: false, // Disable auto revalidation
468
+ }
469
+ );
470
+ ```
471
+
472
+ ## Hook Options
473
+
474
+ ```typescript
475
+ const useAPI = createEnlaceHookReact<ApiSchema>(
476
+ "https://api.example.com",
477
+ {
478
+ // Default fetch options
479
+ headers: { Authorization: "Bearer token" },
480
+ },
481
+ {
482
+ // Hook options
483
+ autoGenerateTags: true, // Auto-generate cache tags from URL
484
+ autoRevalidateTags: true, // Auto-revalidate after mutations
485
+ staleTime: 0, // Cache freshness duration (ms)
486
+ }
487
+ );
488
+ ```
489
+
490
+ ### Async Headers
491
+
492
+ 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):
493
+
494
+ ```typescript
495
+ // Static headers
496
+ const useAPI = createEnlaceHookReact<ApiSchema>("https://api.example.com", {
497
+ headers: { Authorization: "Bearer token" },
498
+ });
499
+
500
+ // Sync function
501
+ const useAPI = createEnlaceHookReact<ApiSchema>("https://api.example.com", {
502
+ headers: () => ({ Authorization: `Bearer ${getToken()}` }),
503
+ });
504
+
505
+ // Async function
506
+ const useAPI = createEnlaceHookReact<ApiSchema>("https://api.example.com", {
507
+ headers: async () => {
508
+ const token = await getTokenFromStorage();
509
+ return { Authorization: `Bearer ${token}` };
510
+ },
511
+ });
512
+ ```
513
+
514
+ This also works for per-request headers:
515
+
516
+ ```typescript
517
+ const { data } = useAPI((api) =>
518
+ api.posts.get({
519
+ headers: async () => {
520
+ const token = await refreshToken();
521
+ return { Authorization: `Bearer ${token}` };
522
+ },
523
+ })
524
+ );
525
+ ```
526
+
527
+ ### Global Callbacks
528
+
529
+ You can set up global `onSuccess` and `onError` callbacks that are called for every request:
530
+
531
+ ```typescript
532
+ const useAPI = createEnlaceHookReact<ApiSchema>(
533
+ "https://api.example.com",
534
+ {
535
+ headers: { Authorization: "Bearer token" },
536
+ },
537
+ {
538
+ onSuccess: (payload) => {
539
+ console.log("Request succeeded:", payload.status, payload.data);
540
+ },
541
+ onError: (payload) => {
542
+ if (payload.status === 0) {
543
+ // Network error
544
+ console.error("Network error:", payload.error.message);
545
+ } else {
546
+ // HTTP error (4xx, 5xx)
547
+ console.error("HTTP error:", payload.status, payload.error);
548
+ }
549
+ },
550
+ }
551
+ );
552
+ ```
553
+
554
+ **Callback Payloads:**
555
+
556
+ ```typescript
557
+ // onSuccess payload
558
+ type EnlaceCallbackPayload<T> = {
559
+ status: number;
560
+ data: T;
561
+ headers: Headers;
562
+ };
563
+
564
+ // onError payload (HTTP error or network error)
565
+ type EnlaceErrorCallbackPayload<T> =
566
+ | { status: number; error: T; headers: Headers } // HTTP error
567
+ | { status: 0; error: Error; headers: null }; // Network error
568
+ ```
569
+
570
+ **Use cases:**
571
+
572
+ - Global error logging/reporting
573
+ - Toast notifications for all API errors
574
+ - Authentication refresh on 401 errors
575
+ - Analytics tracking
576
+
577
+ ## Return Types
578
+
579
+ ### Query Mode
580
+
581
+ ```typescript
582
+ // Basic usage
583
+ const result = useAPI((api) => api.posts.get());
584
+
585
+ // With options
586
+ const result = useAPI(
587
+ (api) => api.posts.get(),
588
+ { enabled: true } // Skip fetching when false
589
+ );
590
+
591
+ type UseEnlaceQueryResult<TData, TError> = {
592
+ loading: boolean; // No cached data and fetching
593
+ fetching: boolean; // Request in progress
594
+ data: TData | undefined;
595
+ error: TError | undefined;
596
+ };
597
+ ```
598
+
599
+ ### Selector Mode
600
+
601
+ ```typescript
602
+ type UseEnlaceSelectorResult<TMethod> = {
603
+ trigger: TMethod; // Function to trigger the request
604
+ loading: boolean;
605
+ fetching: boolean;
606
+ data: TData | undefined;
607
+ error: TError | undefined;
608
+ };
609
+ ```
610
+
611
+ ### Request Options
612
+
613
+ ```typescript
614
+ type RequestOptions = {
615
+ query?: TQuery; // Query parameters (typed when using EndpointWithQuery/EndpointFull)
616
+ body?: TBody; // Request body (JSON)
617
+ formData?: TFormData; // FormData fields (auto-converted, for file uploads)
618
+ headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>); // Request headers
619
+ tags?: string[]; // Cache tags (GET only)
620
+ revalidateTags?: string[]; // Tags to invalidate after mutation
621
+ pathParams?: Record<string, string | number>; // Dynamic path parameters
622
+ };
623
+ ```
624
+
625
+ ---
626
+
627
+ ## Next.js Integration
628
+
629
+ ### Server Components
630
+
631
+ Use `createEnlaceNext` from `enlace` for server components:
632
+
633
+ ```typescript
634
+ import { createEnlaceNext } from "enlace";
635
+
636
+ type ApiError = { message: string };
637
+
638
+ const api = createEnlaceNext<ApiSchema, ApiError>("https://api.example.com", {}, {
639
+ autoGenerateTags: true,
640
+ });
641
+
642
+ export default async function Page() {
643
+ const { data } = await api.posts.get({
644
+ revalidate: 60, // ISR: revalidate every 60 seconds
645
+ });
646
+
647
+ return <PostList posts={data} />;
648
+ }
649
+ ```
650
+
651
+ ### Client Components
652
+
653
+ Use `createEnlaceHookNext` from `enlace/hook` for client components:
654
+
655
+ ```typescript
656
+ "use client";
657
+
658
+ import { createEnlaceHookNext } from "enlace/hook";
659
+
660
+ type ApiError = { message: string };
661
+
662
+ const useAPI = createEnlaceHookNext<ApiSchema, ApiError>(
663
+ "https://api.example.com"
664
+ );
665
+ ```
666
+
667
+ ### Server-Side Revalidation
668
+
669
+ Trigger Next.js cache revalidation after mutations:
670
+
671
+ ```typescript
672
+ // actions.ts
673
+ "use server";
674
+
675
+ import { revalidateTag, revalidatePath } from "next/cache";
676
+
677
+ export async function revalidateAction(tags: string[], paths: string[]) {
678
+ for (const tag of tags) {
679
+ revalidateTag(tag);
680
+ }
681
+ for (const path of paths) {
682
+ revalidatePath(path);
683
+ }
684
+ }
685
+ ```
686
+
687
+ ```typescript
688
+ // useAPI.ts
689
+ import { createEnlaceHookNext } from "enlace/hook";
690
+ import { revalidateAction } from "./actions";
691
+
692
+ type ApiError = { message: string };
693
+
694
+ const useAPI = createEnlaceHookNext<ApiSchema, ApiError>(
695
+ "/api",
696
+ {},
697
+ {
698
+ serverRevalidator: revalidateAction,
699
+ }
700
+ );
701
+ ```
702
+
703
+ **In components:**
704
+
705
+ ```typescript
706
+ function CreatePost() {
707
+ const { trigger } = useAPI((api) => api.posts.post);
708
+
709
+ const handleCreate = () => {
710
+ trigger({
711
+ body: { title: "New Post" },
712
+ revalidateTags: ["posts"], // Passed to serverRevalidator
713
+ revalidatePaths: ["/posts"], // Passed to serverRevalidator
714
+ });
715
+ };
716
+ }
717
+ ```
718
+
719
+ ### CSR-Heavy Projects
720
+
721
+ For projects that primarily use client-side rendering with minimal SSR, you can disable server-side revalidation by default:
722
+
723
+ ```typescript
724
+ const useAPI = createEnlaceHookNext<ApiSchema, ApiError>(
725
+ "/api",
726
+ {},
727
+ {
728
+ serverRevalidator: revalidateAction,
729
+ skipServerRevalidation: true, // Disable server revalidation by default
730
+ }
731
+ );
732
+
733
+ // Mutations won't trigger server revalidation by default
734
+ await trigger({ body: { title: "New Post" } });
735
+
736
+ // Opt-in to server revalidation when needed
737
+ await trigger({ body: { title: "New Post" }, serverRevalidate: true });
738
+ ```
739
+
740
+ ### Per-Request Server Revalidation Control
741
+
742
+ Override the global setting for individual requests:
743
+
744
+ ```typescript
745
+ // Skip server revalidation for this request
746
+ await trigger({ body: data, serverRevalidate: false });
747
+
748
+ // Force server revalidation for this request
749
+ await trigger({ body: data, serverRevalidate: true });
750
+ ```
751
+
752
+ ### Next.js Request Options
753
+
754
+ ```typescript
755
+ api.posts.get({
756
+ tags: ["posts"], // Next.js cache tags
757
+ revalidate: 60, // ISR revalidation (seconds)
758
+ revalidateTags: ["posts"], // Tags to invalidate after mutation
759
+ revalidatePaths: ["/"], // Paths to revalidate after mutation
760
+ serverRevalidate: true, // Control server-side revalidation per-request
761
+ });
762
+ ```
763
+
764
+ ### Relative URLs
765
+
766
+ Works with Next.js API routes:
767
+
768
+ ```typescript
769
+ // Client component calling /api/posts
770
+ const useAPI = createEnlaceHookNext<ApiSchema, ApiError>("/api");
771
+ ```
772
+
773
+ ---
774
+
775
+ ## API Reference
776
+
777
+ ### `createEnlaceHookReact<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
778
+
779
+ Creates a React hook for making API calls.
780
+
781
+ ### `createEnlaceHookNext<TSchema, TDefaultError>(baseUrl, options?, hookOptions?)`
782
+
783
+ Creates a Next.js hook with server revalidation support.
784
+
785
+ ### `createEnlace<TSchema, TDefaultError>(baseUrl, options?, callbacks?)`
786
+
787
+ Creates a typed API client (non-hook, for direct calls or server components).
788
+
789
+ ### `createEnlaceNext<TSchema, TDefaultError>(baseUrl, options?, nextOptions?)`
790
+
791
+ Creates a Next.js typed API client with caching support.
792
+
793
+ **Generic Parameters:**
794
+
795
+ - `TSchema` — API schema type defining endpoints
796
+ - `TDefaultError` — Default error type for all endpoints (default: `unknown`)
797
+
798
+ **Function Parameters:**
799
+
800
+ - `baseUrl` — Base URL for requests
801
+ - `options` — Default fetch options (headers, cache, etc.)
802
+ - `hookOptions` / `callbacks` / `nextOptions` — Additional configuration
803
+
804
+ **Hook Options:**
805
+
806
+ ```typescript
807
+ type EnlaceHookOptions = {
808
+ autoGenerateTags?: boolean; // default: true
809
+ autoRevalidateTags?: boolean; // default: true
810
+ staleTime?: number; // default: 0
811
+ onSuccess?: (payload: EnlaceCallbackPayload<unknown>) => void;
812
+ onError?: (payload: EnlaceErrorCallbackPayload<unknown>) => void;
813
+ };
814
+ ```
815
+
816
+ ### Re-exports from enlace-core
817
+
818
+ - `Endpoint` — Type helper for endpoints with JSON body
819
+ - `EndpointWithQuery` — Type helper for endpoints with typed query params
820
+ - `EndpointWithFormData` — Type helper for file upload endpoints
821
+ - `EndpointFull` — Object-style type helper for complex endpoints
822
+ - `EnlaceResponse` — Response type
823
+ - `EnlaceOptions` — Fetch options type
824
+
825
+ ## OpenAPI Generation
826
+
827
+ Generate OpenAPI 3.0 specs from your TypeScript schema using [`enlace-openapi`](../openapi/README.md):
828
+
829
+ ```bash
830
+ npm install enlace-openapi
831
+ enlace-openapi --schema ./types/APISchema.ts --output ./openapi.json
832
+ ```
833
+
834
+ ## License
835
+
836
+ MIT