@simplix-react/contract 0.0.1 → 0.0.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.
- package/README.md +309 -0
- package/dist/index.d.ts +606 -11
- package/dist/index.js +99 -12
- package/package.json +19 -20
- package/dist/mcp-tools.d.ts +0 -14
- package/dist/mcp-tools.js +0 -62
- package/dist/openapi.d.ts +0 -21
- package/dist/openapi.js +0 -54
- package/dist/types-tFXBXgJP.d.ts +0 -41
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# @simplix-react/contract
|
|
2
|
+
|
|
3
|
+
Define type-safe API contracts with Zod schemas. A single contract drives your HTTP client, React Query hooks, and MSW mock handlers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @simplix-react/contract
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer dependency:** `zod >= 4.0.0`
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add zod
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Example
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { defineApi, simpleQueryBuilder } from "@simplix-react/contract";
|
|
22
|
+
|
|
23
|
+
// 1. Define your contract
|
|
24
|
+
const projectApi = defineApi({
|
|
25
|
+
domain: "project",
|
|
26
|
+
basePath: "/api/v1",
|
|
27
|
+
entities: {
|
|
28
|
+
task: {
|
|
29
|
+
path: "/tasks",
|
|
30
|
+
schema: z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
title: z.string(),
|
|
33
|
+
status: z.enum(["open", "closed"]),
|
|
34
|
+
}),
|
|
35
|
+
createSchema: z.object({
|
|
36
|
+
title: z.string(),
|
|
37
|
+
}),
|
|
38
|
+
updateSchema: z.object({
|
|
39
|
+
title: z.string().optional(),
|
|
40
|
+
status: z.enum(["open", "closed"]).optional(),
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
queryBuilder: simpleQueryBuilder,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 2. Use the type-safe client
|
|
48
|
+
const tasks = await projectApi.client.task.list();
|
|
49
|
+
const task = await projectApi.client.task.get("task-1");
|
|
50
|
+
const created = await projectApi.client.task.create({ title: "New task" });
|
|
51
|
+
const updated = await projectApi.client.task.update("task-1", { status: "closed" });
|
|
52
|
+
await projectApi.client.task.delete("task-1");
|
|
53
|
+
|
|
54
|
+
// 3. Use query keys for cache management
|
|
55
|
+
projectApi.queryKeys.task.all; // ["project", "task"]
|
|
56
|
+
projectApi.queryKeys.task.lists(); // ["project", "task", "list"]
|
|
57
|
+
projectApi.queryKeys.task.list({ status: "open" }); // ["project", "task", "list", { status: "open" }]
|
|
58
|
+
projectApi.queryKeys.task.detail("task-1"); // ["project", "task", "detail", "task-1"]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API Overview
|
|
62
|
+
|
|
63
|
+
| Export | Kind | Description |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `defineApi` | Function | Creates a contract with client and query keys from a config |
|
|
66
|
+
| `deriveClient` | Function | Generates a type-safe HTTP client from a contract config |
|
|
67
|
+
| `deriveQueryKeys` | Function | Generates TanStack Query key factories from a contract config |
|
|
68
|
+
| `buildPath` | Function | Substitutes `:param` placeholders in URL templates |
|
|
69
|
+
| `defaultFetch` | Function | Built-in fetch with JSON content-type and `{ data }` envelope unwrapping |
|
|
70
|
+
| `ApiError` | Class | Error type for non-2xx HTTP responses |
|
|
71
|
+
| `simpleQueryBuilder` | Object | Ready-made `QueryBuilder` for common REST query string patterns |
|
|
72
|
+
| `camelToKebab` | Function | Converts camelCase to kebab-case |
|
|
73
|
+
| `camelToSnake` | Function | Converts camelCase to snake_case |
|
|
74
|
+
|
|
75
|
+
### Type Exports
|
|
76
|
+
|
|
77
|
+
| Export | Description |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `EntityDefinition` | Describes a CRUD entity with Zod schemas |
|
|
80
|
+
| `EntityParent` | Parent resource for nested URL construction |
|
|
81
|
+
| `EntityQuery` | Named query scope filtering by parent relationship |
|
|
82
|
+
| `OperationDefinition` | Describes a custom non-CRUD API operation |
|
|
83
|
+
| `HttpMethod` | Union of supported HTTP methods |
|
|
84
|
+
| `ApiContractConfig` | Full configuration input for `defineApi` |
|
|
85
|
+
| `ApiContract` | Return type of `defineApi` |
|
|
86
|
+
| `EntityClient` | CRUD client interface for a single entity |
|
|
87
|
+
| `QueryKeyFactory` | Structured query key generators for TanStack Query |
|
|
88
|
+
| `FetchFn` | Custom fetch function signature |
|
|
89
|
+
| `ListParams` | Filters, sort, and pagination parameters for list queries |
|
|
90
|
+
| `SortParam` | Sort field and direction |
|
|
91
|
+
| `PaginationParam` | Offset-based or cursor-based pagination |
|
|
92
|
+
| `PageInfo` | Server-returned pagination metadata |
|
|
93
|
+
| `QueryBuilder` | Interface for serializing list params to URL search params |
|
|
94
|
+
|
|
95
|
+
## Key Concepts
|
|
96
|
+
|
|
97
|
+
### Zod Schema → Type Inference
|
|
98
|
+
|
|
99
|
+
Every contract type is inferred from Zod schemas at compile time. You define schemas once; TypeScript infers the rest:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { z } from "zod";
|
|
103
|
+
|
|
104
|
+
const taskSchema = z.object({
|
|
105
|
+
id: z.string(),
|
|
106
|
+
title: z.string(),
|
|
107
|
+
status: z.enum(["open", "closed"]),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// z.infer<typeof taskSchema> → { id: string; title: string; status: "open" | "closed" }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The framework uses these inferred types throughout the client, hooks, and mock handlers, so your API types are always in sync with validation logic.
|
|
114
|
+
|
|
115
|
+
### EntityDefinition
|
|
116
|
+
|
|
117
|
+
An `EntityDefinition` is the building block for CRUD resources. It bundles three schemas:
|
|
118
|
+
|
|
119
|
+
- **`schema`** — The full entity shape returned by the API
|
|
120
|
+
- **`createSchema`** — The payload required to create a new entity
|
|
121
|
+
- **`updateSchema`** — The payload for partial updates
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const taskEntity = {
|
|
125
|
+
path: "/tasks",
|
|
126
|
+
schema: taskSchema,
|
|
127
|
+
createSchema: z.object({ title: z.string() }),
|
|
128
|
+
updateSchema: z.object({ title: z.string().optional() }),
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This single definition drives `list`, `get`, `create`, `update`, and `delete` methods on the client.
|
|
133
|
+
|
|
134
|
+
### OperationDefinition
|
|
135
|
+
|
|
136
|
+
For endpoints that don't fit the CRUD pattern (file uploads, RPC-style calls, batch operations), use `OperationDefinition`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { z } from "zod";
|
|
140
|
+
import { defineApi } from "@simplix-react/contract";
|
|
141
|
+
|
|
142
|
+
const api = defineApi({
|
|
143
|
+
domain: "project",
|
|
144
|
+
basePath: "/api/v1",
|
|
145
|
+
entities: { task: taskEntity },
|
|
146
|
+
operations: {
|
|
147
|
+
assignTask: {
|
|
148
|
+
method: "POST",
|
|
149
|
+
path: "/tasks/:taskId/assign",
|
|
150
|
+
input: z.object({ userId: z.string() }),
|
|
151
|
+
output: z.object({ id: z.string(), assigneeId: z.string() }),
|
|
152
|
+
},
|
|
153
|
+
exportReport: {
|
|
154
|
+
method: "GET",
|
|
155
|
+
path: "/projects/:projectId/export",
|
|
156
|
+
input: z.object({}),
|
|
157
|
+
output: z.any(),
|
|
158
|
+
responseType: "blob",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Path params are positional arguments, input is the last argument
|
|
164
|
+
await api.client.assignTask("task-1", { userId: "user-42" });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Nested Entities (Parent Relationships)
|
|
168
|
+
|
|
169
|
+
Entities can declare a `parent` for nested URL construction:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const taskEntity = {
|
|
173
|
+
path: "/tasks",
|
|
174
|
+
schema: taskSchema,
|
|
175
|
+
createSchema: createTaskSchema,
|
|
176
|
+
updateSchema: updateTaskSchema,
|
|
177
|
+
parent: { param: "projectId", path: "/projects" },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Client adjusts URLs based on parent
|
|
181
|
+
await api.client.task.list("project-1");
|
|
182
|
+
// GET /api/v1/projects/project-1/tasks
|
|
183
|
+
|
|
184
|
+
await api.client.task.create("project-1", { title: "New task" });
|
|
185
|
+
// POST /api/v1/projects/project-1/tasks
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Query Keys
|
|
189
|
+
|
|
190
|
+
The contract automatically generates TanStack Query-compatible key factories with hierarchical structure:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
task.all → ["project", "task"] (broadest)
|
|
194
|
+
task.lists() → ["project", "task", "list"]
|
|
195
|
+
task.list({ ... }) → ["project", "task", "list", { ... }]
|
|
196
|
+
task.details() → ["project", "task", "detail"]
|
|
197
|
+
task.detail("id") → ["project", "task", "detail", "id"] (most specific)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Invalidating a broader key automatically invalidates all more-specific keys beneath it.
|
|
201
|
+
|
|
202
|
+
## Guides
|
|
203
|
+
|
|
204
|
+
### Custom Fetch Function
|
|
205
|
+
|
|
206
|
+
Replace the built-in HTTP client with custom logic for authentication, logging, or retry:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { defineApi, defaultFetch } from "@simplix-react/contract";
|
|
210
|
+
|
|
211
|
+
const api = defineApi(config, {
|
|
212
|
+
fetchFn: async (path, options) => {
|
|
213
|
+
const token = await getAuthToken();
|
|
214
|
+
return defaultFetch(path, {
|
|
215
|
+
...options,
|
|
216
|
+
headers: {
|
|
217
|
+
...options?.headers,
|
|
218
|
+
Authorization: `Bearer ${token}`,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Custom Query Builder
|
|
226
|
+
|
|
227
|
+
Implement the `QueryBuilder` interface to match your API's query string conventions:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { defineApi } from "@simplix-react/contract";
|
|
231
|
+
import type { QueryBuilder } from "@simplix-react/contract";
|
|
232
|
+
|
|
233
|
+
const springQueryBuilder: QueryBuilder = {
|
|
234
|
+
buildSearchParams(params) {
|
|
235
|
+
const sp = new URLSearchParams();
|
|
236
|
+
if (params.pagination?.type === "offset") {
|
|
237
|
+
// Spring uses 0-based page indexing
|
|
238
|
+
sp.set("page", String(params.pagination.page - 1));
|
|
239
|
+
sp.set("size", String(params.pagination.limit));
|
|
240
|
+
}
|
|
241
|
+
if (params.sort) {
|
|
242
|
+
const sorts = Array.isArray(params.sort) ? params.sort : [params.sort];
|
|
243
|
+
for (const s of sorts) {
|
|
244
|
+
sp.append("sort", `${s.field},${s.direction}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return sp;
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const api = defineApi({
|
|
252
|
+
domain: "project",
|
|
253
|
+
basePath: "/api/v1",
|
|
254
|
+
entities: { task: taskEntity },
|
|
255
|
+
queryBuilder: springQueryBuilder,
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Multipart File Uploads
|
|
260
|
+
|
|
261
|
+
Use `contentType: "multipart"` in an operation definition for file upload endpoints:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const api = defineApi({
|
|
265
|
+
domain: "project",
|
|
266
|
+
basePath: "/api/v1",
|
|
267
|
+
entities: {},
|
|
268
|
+
operations: {
|
|
269
|
+
uploadAvatar: {
|
|
270
|
+
method: "POST",
|
|
271
|
+
path: "/users/:userId/avatar",
|
|
272
|
+
input: z.object({ file: z.instanceof(File) }),
|
|
273
|
+
output: z.object({ url: z.string() }),
|
|
274
|
+
contentType: "multipart",
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await api.client.uploadAvatar("user-1", { file: selectedFile });
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Error Handling
|
|
283
|
+
|
|
284
|
+
All client methods throw `ApiError` for non-2xx responses:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { ApiError } from "@simplix-react/contract";
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await api.client.task.get("nonexistent");
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error instanceof ApiError) {
|
|
293
|
+
console.log(error.status); // 404
|
|
294
|
+
console.log(error.body); // Raw response body
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Related Packages
|
|
300
|
+
|
|
301
|
+
| Package | Description |
|
|
302
|
+
| --- | --- |
|
|
303
|
+
| `@simplix-react/react` | Derives React Query hooks from the contract |
|
|
304
|
+
| `@simplix-react/mock` | Generates MSW handlers and PGlite repositories from the contract |
|
|
305
|
+
| `@simplix-react/testing` | Test utilities built on top of the contract and mock packages |
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
**Next Step** → [`@simplix-react/react`](../react/README.md) — Turn your contract into React Query hooks.
|