enlace 0.0.1-beta.1 → 0.0.1-beta.2
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 +417 -0
- 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,417 @@
|
|
|
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[]>;
|
|
19
|
+
$post: Endpoint<Post, ApiError, CreatePost>;
|
|
20
|
+
_: {
|
|
21
|
+
$get: Endpoint<Post>;
|
|
22
|
+
$delete: Endpoint<void>;
|
|
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[]>; // GET /users
|
|
56
|
+
$post: Endpoint<User>; // POST /users
|
|
57
|
+
_: { // /users/:id
|
|
58
|
+
$get: Endpoint<User>; // GET /users/:id
|
|
59
|
+
$put: Endpoint<User>; // PUT /users/:id
|
|
60
|
+
$delete: Endpoint<void>; // DELETE /users/:id
|
|
61
|
+
profile: {
|
|
62
|
+
$get: Endpoint<Profile>; // 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 = unknown, TBody = never> = {
|
|
78
|
+
data: TData; // Response data type
|
|
79
|
+
error: TError; // Error response type
|
|
80
|
+
body: TBody; // Request body type
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Examples
|
|
84
|
+
type GetUsers = Endpoint<User[]>; // 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
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
function Post({ id }: { id: number }) {
|
|
121
|
+
// Automatically re-fetches when `id` or query values change
|
|
122
|
+
const { data } = useAPI((api) => api.posts[id].get({ query: { include: "author" } }));
|
|
123
|
+
return <div>{data?.title}</div>;
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Selector Mode (Manual Trigger)
|
|
128
|
+
|
|
129
|
+
For mutations or lazy-loaded requests:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
function DeleteButton({ id }: { id: number }) {
|
|
133
|
+
const { trigger, loading } = useAPI((api) => api.posts[id].delete);
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<button onClick={() => trigger()} disabled={loading}>
|
|
137
|
+
{loading ? "Deleting..." : "Delete"}
|
|
138
|
+
</button>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**With request body:**
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
function CreatePost() {
|
|
147
|
+
const { trigger, loading, data } = useAPI((api) => api.posts.post);
|
|
148
|
+
|
|
149
|
+
const handleSubmit = async (title: string) => {
|
|
150
|
+
const result = await trigger({ body: { title } });
|
|
151
|
+
if (result.ok) {
|
|
152
|
+
console.log("Created:", result.data);
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return <button onClick={() => handleSubmit("New Post")}>Create</button>;
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Caching & Auto-Revalidation
|
|
161
|
+
|
|
162
|
+
### Automatic Cache Tags (Zero Config)
|
|
163
|
+
|
|
164
|
+
**Tags are automatically generated from URL paths** — no manual configuration needed:
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
// GET /posts → tags: ['posts']
|
|
168
|
+
// GET /posts/123 → tags: ['posts', 'posts/123']
|
|
169
|
+
// GET /users/5/posts → tags: ['users', 'users/5', 'users/5/posts']
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Mutations automatically revalidate matching tags:**
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
const { trigger } = useAPI((api) => api.posts.post);
|
|
176
|
+
|
|
177
|
+
// POST /posts automatically revalidates 'posts' tag
|
|
178
|
+
// All queries with 'posts' tag will refetch!
|
|
179
|
+
trigger({ body: { title: "New Post" } });
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
This means in most cases, **you don't need to specify any tags manually**. The cache just works.
|
|
183
|
+
|
|
184
|
+
### How It Works
|
|
185
|
+
|
|
186
|
+
1. **Queries** automatically cache with tags derived from the URL
|
|
187
|
+
2. **Mutations** automatically revalidate tags derived from the URL
|
|
188
|
+
3. All queries matching those tags refetch automatically
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// Component A: fetches posts (cached with tag 'posts')
|
|
192
|
+
const { data } = useAPI((api) => api.posts.get());
|
|
193
|
+
|
|
194
|
+
// Component B: creates a post
|
|
195
|
+
const { trigger } = useAPI((api) => api.posts.post);
|
|
196
|
+
trigger({ body: { title: "New" } });
|
|
197
|
+
// → Automatically revalidates 'posts' tag
|
|
198
|
+
// → Component A refetches automatically!
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Stale Time
|
|
202
|
+
|
|
203
|
+
Control how long cached data is considered fresh:
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com", {}, {
|
|
207
|
+
staleTime: 5000, // 5 seconds
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- `staleTime: 0` (default) — Always revalidate on mount
|
|
212
|
+
- `staleTime: 5000` — Data is fresh for 5 seconds
|
|
213
|
+
- `staleTime: Infinity` — Never revalidate automatically
|
|
214
|
+
|
|
215
|
+
### Manual Tag Override (Optional)
|
|
216
|
+
|
|
217
|
+
Override auto-generated tags when needed:
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// Custom cache tags
|
|
221
|
+
const { data } = useAPI((api) => api.posts.get({ tags: ["my-custom-tag"] }));
|
|
222
|
+
|
|
223
|
+
// Custom revalidation tags
|
|
224
|
+
trigger({
|
|
225
|
+
body: { title: "New" },
|
|
226
|
+
revalidateTags: ["posts", "dashboard"], // Override auto-generated
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Disable Auto-Revalidation
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com", {}, {
|
|
234
|
+
autoGenerateTags: false, // Disable auto tag generation
|
|
235
|
+
autoRevalidateTags: false, // Disable auto revalidation
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Hook Options
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
const useAPI = createEnlaceHook<ApiSchema>(
|
|
243
|
+
"https://api.example.com",
|
|
244
|
+
{
|
|
245
|
+
// Default fetch options
|
|
246
|
+
headers: { Authorization: "Bearer token" },
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
// Hook options
|
|
250
|
+
autoGenerateTags: true, // Auto-generate cache tags from URL
|
|
251
|
+
autoRevalidateTags: true, // Auto-revalidate after mutations
|
|
252
|
+
staleTime: 0, // Cache freshness duration (ms)
|
|
253
|
+
}
|
|
254
|
+
);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Return Types
|
|
258
|
+
|
|
259
|
+
### Query Mode
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
type UseEnlaceQueryResult<TData, TError> = {
|
|
263
|
+
loading: boolean; // No cached data and fetching
|
|
264
|
+
fetching: boolean; // Request in progress
|
|
265
|
+
ok: boolean | undefined;
|
|
266
|
+
data: TData | undefined;
|
|
267
|
+
error: TError | undefined;
|
|
268
|
+
};
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Selector Mode
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
type UseEnlaceSelectorResult<TMethod> = {
|
|
275
|
+
trigger: TMethod; // Function to trigger the request
|
|
276
|
+
loading: boolean;
|
|
277
|
+
fetching: boolean;
|
|
278
|
+
ok: boolean | undefined;
|
|
279
|
+
data: TData | undefined;
|
|
280
|
+
error: TError | undefined;
|
|
281
|
+
};
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Next.js Integration
|
|
287
|
+
|
|
288
|
+
### Server Components
|
|
289
|
+
|
|
290
|
+
Use `createEnlace` from `enlace/next` for server components:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
import { createEnlace } from "enlace/next";
|
|
294
|
+
|
|
295
|
+
const api = createEnlace<ApiSchema>("https://api.example.com", {}, {
|
|
296
|
+
autoGenerateTags: true,
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
export default async function Page() {
|
|
300
|
+
const { data } = await api.posts.get({
|
|
301
|
+
revalidate: 60, // ISR: revalidate every 60 seconds
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
return <PostList posts={data} />;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Client Components
|
|
309
|
+
|
|
310
|
+
Use `createEnlaceHook` from `enlace/next/hook` for client components:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
"use client";
|
|
314
|
+
|
|
315
|
+
import { createEnlaceHook } from "enlace/next/hook";
|
|
316
|
+
|
|
317
|
+
const useAPI = createEnlaceHook<ApiSchema>("https://api.example.com");
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Server-Side Revalidation
|
|
321
|
+
|
|
322
|
+
Trigger Next.js cache revalidation after mutations:
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// actions.ts
|
|
326
|
+
"use server";
|
|
327
|
+
|
|
328
|
+
import { revalidateTag, revalidatePath } from "next/cache";
|
|
329
|
+
|
|
330
|
+
export async function revalidateAction(tags: string[], paths: string[]) {
|
|
331
|
+
for (const tag of tags) {
|
|
332
|
+
revalidateTag(tag);
|
|
333
|
+
}
|
|
334
|
+
for (const path of paths) {
|
|
335
|
+
revalidatePath(path);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
// useAPI.ts
|
|
342
|
+
import { createEnlaceHook } from "enlace/next/hook";
|
|
343
|
+
import { revalidateAction } from "./actions";
|
|
344
|
+
|
|
345
|
+
const useAPI = createEnlaceHook<ApiSchema>("/api", {}, {
|
|
346
|
+
revalidator: revalidateAction,
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**In components:**
|
|
351
|
+
|
|
352
|
+
```typescript
|
|
353
|
+
function CreatePost() {
|
|
354
|
+
const { trigger } = useAPI((api) => api.posts.post);
|
|
355
|
+
|
|
356
|
+
const handleCreate = () => {
|
|
357
|
+
trigger({
|
|
358
|
+
body: { title: "New Post" },
|
|
359
|
+
revalidateTags: ["posts"], // Passed to revalidator
|
|
360
|
+
revalidatePaths: ["/posts"], // Passed to revalidator
|
|
361
|
+
});
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Next.js Request Options
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
api.posts.get({
|
|
370
|
+
tags: ["posts"], // Next.js cache tags
|
|
371
|
+
revalidate: 60, // ISR revalidation (seconds)
|
|
372
|
+
revalidateTags: ["posts"], // Tags to invalidate after mutation
|
|
373
|
+
revalidatePaths: ["/"], // Paths to revalidate after mutation
|
|
374
|
+
skipRevalidator: false, // Skip server-side revalidation
|
|
375
|
+
});
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Relative URLs
|
|
379
|
+
|
|
380
|
+
Works with Next.js API routes:
|
|
381
|
+
|
|
382
|
+
```typescript
|
|
383
|
+
// Client component calling /api/posts
|
|
384
|
+
const useAPI = createEnlaceHook<ApiSchema>("/api");
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## API Reference
|
|
390
|
+
|
|
391
|
+
### `createEnlaceHook<TSchema>(baseUrl, options?, hookOptions?)`
|
|
392
|
+
|
|
393
|
+
Creates a React hook for making API calls.
|
|
394
|
+
|
|
395
|
+
**Parameters:**
|
|
396
|
+
- `baseUrl` — Base URL for requests
|
|
397
|
+
- `options` — Default fetch options (headers, cache, etc.)
|
|
398
|
+
- `hookOptions` — Hook configuration
|
|
399
|
+
|
|
400
|
+
**Hook Options:**
|
|
401
|
+
```typescript
|
|
402
|
+
type EnlaceHookOptions = {
|
|
403
|
+
autoGenerateTags?: boolean; // default: true
|
|
404
|
+
autoRevalidateTags?: boolean; // default: true
|
|
405
|
+
staleTime?: number; // default: 0
|
|
406
|
+
};
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### Re-exports from enlace-core
|
|
410
|
+
|
|
411
|
+
- `Endpoint` — Type helper for schema definition
|
|
412
|
+
- `EnlaceResponse` — Response type
|
|
413
|
+
- `EnlaceOptions` — Fetch options type
|
|
414
|
+
|
|
415
|
+
## License
|
|
416
|
+
|
|
417
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "enlace",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.2",
|
|
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.
|
|
26
|
+
"enlace-core": "0.0.1-beta.2"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"react": "^19"
|