enlace 0.0.1-beta.1 → 0.0.1-beta.3

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.
Files changed (3) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +446 -0
  3. package/package.json +2 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Enlace
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,446 @@
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 { createEnlaceHook, Endpoint } from "enlace";
15
+
16
+ type ApiSchema = {
17
+ posts: {
18
+ $get: Endpoint<Post[], ApiError>;
19
+ $post: Endpoint<Post, ApiError, CreatePost>;
20
+ _: {
21
+ $get: Endpoint<Post, ApiError>;
22
+ $delete: Endpoint<void, ApiError>;
23
+ };
24
+ };
25
+ };
26
+
27
+ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com");
28
+ ```
29
+
30
+ ## Schema Conventions
31
+
32
+ Defining a schema is **recommended** for full type safety, but **optional**. You can go without types:
33
+
34
+ ```typescript
35
+ // Without schema (untyped, but still works!)
36
+ const useAPI = createEnlaceHook("https://api.example.com");
37
+ const { data } = useAPI((api) => api.any.path.you.want.get());
38
+ ```
39
+
40
+ ```typescript
41
+ // With schema (recommended for type safety)
42
+ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com");
43
+ ```
44
+
45
+ ### Schema Structure
46
+
47
+ - `$get`, `$post`, `$put`, `$patch`, `$delete` — HTTP method endpoints
48
+ - `_` — Dynamic path segment (e.g., `/users/:id`)
49
+
50
+ ```typescript
51
+ import { Endpoint } from "enlace";
52
+
53
+ type ApiSchema = {
54
+ users: {
55
+ $get: Endpoint<User[], ApiError>; // GET /users
56
+ $post: Endpoint<User, ApiError>; // POST /users
57
+ _: { // /users/:id
58
+ $get: Endpoint<User, ApiError>; // GET /users/:id
59
+ $put: Endpoint<User, ApiError>; // PUT /users/:id
60
+ $delete: Endpoint<void, ApiError>; // DELETE /users/:id
61
+ profile: {
62
+ $get: Endpoint<Profile, ApiError>; // GET /users/:id/profile
63
+ };
64
+ };
65
+ };
66
+ };
67
+
68
+ // Usage
69
+ api.users.get(); // GET /users
70
+ api.users[123].get(); // GET /users/123
71
+ api.users[123].profile.get(); // GET /users/123/profile
72
+ ```
73
+
74
+ ### Endpoint Type
75
+
76
+ ```typescript
77
+ type Endpoint<TData, TError, TBody = never> = {
78
+ data: TData; // Response data type
79
+ error: TError; // Error response type (required)
80
+ body: TBody; // Request body type (optional)
81
+ };
82
+
83
+ // Examples
84
+ type GetUsers = Endpoint<User[], ApiError>; // GET, no body
85
+ type CreateUser = Endpoint<User, ApiError, CreateUserInput>; // POST with body
86
+ type DeleteUser = Endpoint<void, NotFoundError>; // DELETE, no response data
87
+ ```
88
+
89
+ ## React Hooks
90
+
91
+ ### Query Mode (Auto-Fetch)
92
+
93
+ For GET requests that fetch data automatically:
94
+
95
+ ```typescript
96
+ function Posts({ page, limit }: { page: number; limit: number }) {
97
+ const { data, loading, error, ok } = useAPI((api) =>
98
+ api.posts.get({ query: { page, limit, published: true } })
99
+ );
100
+
101
+ if (loading) return <div>Loading...</div>;
102
+ if (!ok) return <div>Error: {error.message}</div>;
103
+
104
+ return (
105
+ <ul>
106
+ {data.map((post) => (
107
+ <li key={post.id}>{post.title}</li>
108
+ ))}
109
+ </ul>
110
+ );
111
+ }
112
+ ```
113
+
114
+ **Features:**
115
+ - Auto-fetches on mount
116
+ - Re-fetches when dependencies change (no deps array needed!)
117
+ - Returns cached data while revalidating
118
+ - **Request deduplication** — identical requests from multiple components trigger only one fetch
119
+
120
+ ```typescript
121
+ function Post({ id }: { id: number }) {
122
+ // Automatically re-fetches when `id` or query values change
123
+ const { data } = useAPI((api) => api.posts[id].get({ query: { include: "author" } }));
124
+ return <div>{data?.title}</div>;
125
+ }
126
+ ```
127
+
128
+ ### Request Deduplication
129
+
130
+ Multiple components requesting the same data will share a single network request:
131
+
132
+ ```typescript
133
+ // Both components render at the same time
134
+ function PostTitle({ id }: { id: number }) {
135
+ const { data } = useAPI((api) => api.posts[id].get());
136
+ return <h1>{data?.title}</h1>;
137
+ }
138
+
139
+ function PostBody({ id }: { id: number }) {
140
+ const { data } = useAPI((api) => api.posts[id].get());
141
+ return <p>{data?.body}</p>;
142
+ }
143
+
144
+ // Only ONE fetch request is made to GET /posts/123
145
+ // Both components share the same cached result
146
+ function PostPage() {
147
+ return (
148
+ <>
149
+ <PostTitle id={123} />
150
+ <PostBody id={123} />
151
+ </>
152
+ );
153
+ }
154
+ ```
155
+
156
+ ### Selector Mode (Manual Trigger)
157
+
158
+ For mutations or lazy-loaded requests:
159
+
160
+ ```typescript
161
+ function DeleteButton({ id }: { id: number }) {
162
+ const { trigger, loading } = useAPI((api) => api.posts[id].delete);
163
+
164
+ return (
165
+ <button onClick={() => trigger()} disabled={loading}>
166
+ {loading ? "Deleting..." : "Delete"}
167
+ </button>
168
+ );
169
+ }
170
+ ```
171
+
172
+ **With request body:**
173
+
174
+ ```typescript
175
+ function CreatePost() {
176
+ const { trigger, loading, data } = useAPI((api) => api.posts.post);
177
+
178
+ const handleSubmit = async (title: string) => {
179
+ const result = await trigger({ body: { title } });
180
+ if (result.ok) {
181
+ console.log("Created:", result.data);
182
+ }
183
+ };
184
+
185
+ return <button onClick={() => handleSubmit("New Post")}>Create</button>;
186
+ }
187
+ ```
188
+
189
+ ## Caching & Auto-Revalidation
190
+
191
+ ### Automatic Cache Tags (Zero Config)
192
+
193
+ **Tags are automatically generated from URL paths** — no manual configuration needed:
194
+
195
+ ```typescript
196
+ // GET /posts → tags: ['posts']
197
+ // GET /posts/123 → tags: ['posts', 'posts/123']
198
+ // GET /users/5/posts → tags: ['users', 'users/5', 'users/5/posts']
199
+ ```
200
+
201
+ **Mutations automatically revalidate matching tags:**
202
+
203
+ ```typescript
204
+ const { trigger } = useAPI((api) => api.posts.post);
205
+
206
+ // POST /posts automatically revalidates 'posts' tag
207
+ // All queries with 'posts' tag will refetch!
208
+ trigger({ body: { title: "New Post" } });
209
+ ```
210
+
211
+ This means in most cases, **you don't need to specify any tags manually**. The cache just works.
212
+
213
+ ### How It Works
214
+
215
+ 1. **Queries** automatically cache with tags derived from the URL
216
+ 2. **Mutations** automatically revalidate tags derived from the URL
217
+ 3. All queries matching those tags refetch automatically
218
+
219
+ ```typescript
220
+ // Component A: fetches posts (cached with tag 'posts')
221
+ const { data } = useAPI((api) => api.posts.get());
222
+
223
+ // Component B: creates a post
224
+ const { trigger } = useAPI((api) => api.posts.post);
225
+ trigger({ body: { title: "New" } });
226
+ // → Automatically revalidates 'posts' tag
227
+ // → Component A refetches automatically!
228
+ ```
229
+
230
+ ### Stale Time
231
+
232
+ Control how long cached data is considered fresh:
233
+
234
+ ```typescript
235
+ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com", {}, {
236
+ staleTime: 5000, // 5 seconds
237
+ });
238
+ ```
239
+
240
+ - `staleTime: 0` (default) — Always revalidate on mount
241
+ - `staleTime: 5000` — Data is fresh for 5 seconds
242
+ - `staleTime: Infinity` — Never revalidate automatically
243
+
244
+ ### Manual Tag Override (Optional)
245
+
246
+ Override auto-generated tags when needed:
247
+
248
+ ```typescript
249
+ // Custom cache tags
250
+ const { data } = useAPI((api) => api.posts.get({ tags: ["my-custom-tag"] }));
251
+
252
+ // Custom revalidation tags
253
+ trigger({
254
+ body: { title: "New" },
255
+ revalidateTags: ["posts", "dashboard"], // Override auto-generated
256
+ });
257
+ ```
258
+
259
+ ### Disable Auto-Revalidation
260
+
261
+ ```typescript
262
+ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com", {}, {
263
+ autoGenerateTags: false, // Disable auto tag generation
264
+ autoRevalidateTags: false, // Disable auto revalidation
265
+ });
266
+ ```
267
+
268
+ ## Hook Options
269
+
270
+ ```typescript
271
+ const useAPI = createEnlaceHook<ApiSchema>(
272
+ "https://api.example.com",
273
+ {
274
+ // Default fetch options
275
+ headers: { Authorization: "Bearer token" },
276
+ },
277
+ {
278
+ // Hook options
279
+ autoGenerateTags: true, // Auto-generate cache tags from URL
280
+ autoRevalidateTags: true, // Auto-revalidate after mutations
281
+ staleTime: 0, // Cache freshness duration (ms)
282
+ }
283
+ );
284
+ ```
285
+
286
+ ## Return Types
287
+
288
+ ### Query Mode
289
+
290
+ ```typescript
291
+ type UseEnlaceQueryResult<TData, TError> = {
292
+ loading: boolean; // No cached data and fetching
293
+ fetching: boolean; // Request in progress
294
+ ok: boolean | undefined;
295
+ data: TData | undefined;
296
+ error: TError | undefined;
297
+ };
298
+ ```
299
+
300
+ ### Selector Mode
301
+
302
+ ```typescript
303
+ type UseEnlaceSelectorResult<TMethod> = {
304
+ trigger: TMethod; // Function to trigger the request
305
+ loading: boolean;
306
+ fetching: boolean;
307
+ ok: boolean | undefined;
308
+ data: TData | undefined;
309
+ error: TError | undefined;
310
+ };
311
+ ```
312
+
313
+ ---
314
+
315
+ ## Next.js Integration
316
+
317
+ ### Server Components
318
+
319
+ Use `createEnlace` from `enlace/next` for server components:
320
+
321
+ ```typescript
322
+ import { createEnlace } from "enlace/next";
323
+
324
+ const api = createEnlace<ApiSchema>("https://api.example.com", {}, {
325
+ autoGenerateTags: true,
326
+ });
327
+
328
+ export default async function Page() {
329
+ const { data } = await api.posts.get({
330
+ revalidate: 60, // ISR: revalidate every 60 seconds
331
+ });
332
+
333
+ return <PostList posts={data} />;
334
+ }
335
+ ```
336
+
337
+ ### Client Components
338
+
339
+ Use `createEnlaceHook` from `enlace/next/hook` for client components:
340
+
341
+ ```typescript
342
+ "use client";
343
+
344
+ import { createEnlaceHook } from "enlace/next/hook";
345
+
346
+ const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com");
347
+ ```
348
+
349
+ ### Server-Side Revalidation
350
+
351
+ Trigger Next.js cache revalidation after mutations:
352
+
353
+ ```typescript
354
+ // actions.ts
355
+ "use server";
356
+
357
+ import { revalidateTag, revalidatePath } from "next/cache";
358
+
359
+ export async function revalidateAction(tags: string[], paths: string[]) {
360
+ for (const tag of tags) {
361
+ revalidateTag(tag);
362
+ }
363
+ for (const path of paths) {
364
+ revalidatePath(path);
365
+ }
366
+ }
367
+ ```
368
+
369
+ ```typescript
370
+ // useAPI.ts
371
+ import { createEnlaceHook } from "enlace/next/hook";
372
+ import { revalidateAction } from "./actions";
373
+
374
+ const useAPI = createEnlaceHook<ApiSchema>("/api", {}, {
375
+ revalidator: revalidateAction,
376
+ });
377
+ ```
378
+
379
+ **In components:**
380
+
381
+ ```typescript
382
+ function CreatePost() {
383
+ const { trigger } = useAPI((api) => api.posts.post);
384
+
385
+ const handleCreate = () => {
386
+ trigger({
387
+ body: { title: "New Post" },
388
+ revalidateTags: ["posts"], // Passed to revalidator
389
+ revalidatePaths: ["/posts"], // Passed to revalidator
390
+ });
391
+ };
392
+ }
393
+ ```
394
+
395
+ ### Next.js Request Options
396
+
397
+ ```typescript
398
+ api.posts.get({
399
+ tags: ["posts"], // Next.js cache tags
400
+ revalidate: 60, // ISR revalidation (seconds)
401
+ revalidateTags: ["posts"], // Tags to invalidate after mutation
402
+ revalidatePaths: ["/"], // Paths to revalidate after mutation
403
+ skipRevalidator: false, // Skip server-side revalidation
404
+ });
405
+ ```
406
+
407
+ ### Relative URLs
408
+
409
+ Works with Next.js API routes:
410
+
411
+ ```typescript
412
+ // Client component calling /api/posts
413
+ const useAPI = createEnlaceHook<ApiSchema>("/api");
414
+ ```
415
+
416
+ ---
417
+
418
+ ## API Reference
419
+
420
+ ### `createEnlaceHook<TSchema>(baseUrl, options?, hookOptions?)`
421
+
422
+ Creates a React hook for making API calls.
423
+
424
+ **Parameters:**
425
+ - `baseUrl` — Base URL for requests
426
+ - `options` — Default fetch options (headers, cache, etc.)
427
+ - `hookOptions` — Hook configuration
428
+
429
+ **Hook Options:**
430
+ ```typescript
431
+ type EnlaceHookOptions = {
432
+ autoGenerateTags?: boolean; // default: true
433
+ autoRevalidateTags?: boolean; // default: true
434
+ staleTime?: number; // default: 0
435
+ };
436
+ ```
437
+
438
+ ### Re-exports from enlace-core
439
+
440
+ - `Endpoint` — Type helper for schema definition
441
+ - `EnlaceResponse` — Response type
442
+ - `EnlaceOptions` — Fetch options type
443
+
444
+ ## License
445
+
446
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enlace",
3
- "version": "0.0.1-beta.1",
3
+ "version": "0.0.1-beta.3",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"
@@ -23,7 +23,7 @@
23
23
  }
24
24
  },
25
25
  "dependencies": {
26
- "enlace-core": "0.0.1-beta.1"
26
+ "enlace-core": "0.0.1-beta.2"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "react": "^19"