@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 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.