nuxt-api-contract 0.1.0
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 +385 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +99 -0
- package/dist/client.d.mts +70 -0
- package/dist/client.d.ts +70 -0
- package/dist/client.mjs +3 -0
- package/dist/composables.d.mts +45 -0
- package/dist/composables.d.ts +45 -0
- package/dist/composables.mjs +97 -0
- package/dist/module.d.mts +35 -0
- package/dist/module.d.ts +35 -0
- package/dist/module.mjs +145 -0
- package/dist/openapi.d.mts +46 -0
- package/dist/openapi.d.ts +46 -0
- package/dist/openapi.mjs +312 -0
- package/dist/runtime/server/devtoolsRoute.mjs +6 -0
- package/dist/runtime/server/openapiRoute.mjs +6 -0
- package/dist/runtime/shared/contract.mjs +77 -0
- package/dist/runtime/shared/errors.mjs +71 -0
- package/dist/runtime/shared/format.mjs +32 -0
- package/dist/runtime/shared/serialization.mjs +36 -0
- package/dist/runtime/shared/types.mjs +1 -0
- package/dist/server.d.mts +41 -0
- package/dist/server.d.ts +41 -0
- package/dist/server.mjs +94 -0
- package/dist/shared/nuxt-api-contract.B9JBCRk8.d.mts +37 -0
- package/dist/shared/nuxt-api-contract.CPm9WbWA.d.mts +165 -0
- package/dist/shared/nuxt-api-contract.CPm9WbWA.d.ts +165 -0
- package/dist/shared/nuxt-api-contract.D31EDcwH.d.mts +109 -0
- package/dist/shared/nuxt-api-contract.D31EDcwH.d.ts +109 -0
- package/dist/shared/nuxt-api-contract.DCAU2j7t.mjs +34 -0
- package/dist/shared/nuxt-api-contract.DDpgZj2g.mjs +66 -0
- package/dist/shared/nuxt-api-contract.QDGSGaVY.mjs +117 -0
- package/dist/shared/nuxt-api-contract.S1zqCiJX.d.ts +37 -0
- package/dist/shared/nuxt-api-contract.obS6uV8A.mjs +73 -0
- package/dist/shared.d.mts +4 -0
- package/dist/shared.d.ts +4 -0
- package/dist/shared.mjs +3 -0
- package/dist/testing.d.mts +55 -0
- package/dist/testing.d.ts +55 -0
- package/dist/testing.mjs +49 -0
- package/package.json +104 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nuxt-api-contract contributors
|
|
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,385 @@
|
|
|
1
|
+
# nuxt-api-contract
|
|
2
|
+
|
|
3
|
+
Type-safe API contracts between Nitro server routes and the Nuxt client.
|
|
4
|
+
|
|
5
|
+
Define a contract **once** — get runtime validation, fully typed client calls,
|
|
6
|
+
a unified error format, OpenAPI generation, mocks, contract tests and a
|
|
7
|
+
DevTools panel from the same source of truth.
|
|
8
|
+
|
|
9
|
+
> Status: `0.x` (pre-1.0, SemVer). The public API is intentionally small.
|
|
10
|
+
|
|
11
|
+
## Why
|
|
12
|
+
|
|
13
|
+
Without contracts, the request/response agreement between server and client
|
|
14
|
+
lives in three disconnected places: a Zod schema (or nothing), a TS interface
|
|
15
|
+
(or nothing) and documentation (or nothing). `nuxt-api-contract` collapses all
|
|
16
|
+
of them into one object:
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
┌── runtime validation (server AND response)
|
|
20
|
+
│
|
|
21
|
+
Contract ─────────┼── TypeScript types (params / query / body / response)
|
|
22
|
+
│
|
|
23
|
+
├── OpenAPI document
|
|
24
|
+
│
|
|
25
|
+
├── DevTools panel
|
|
26
|
+
│
|
|
27
|
+
└── mocks + contract tests
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install nuxt-api-contract zod
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Add the module:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
// nuxt.config.ts
|
|
40
|
+
export default defineNuxtConfig({
|
|
41
|
+
modules: ['nuxt-api-contract'],
|
|
42
|
+
apiContract: {
|
|
43
|
+
validateResponse: 'development',
|
|
44
|
+
openapi: { enabled: true, entry: 'contracts/index.ts' },
|
|
45
|
+
devtools: true,
|
|
46
|
+
mocks: false,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Zod (`^3.23`) is a peer dependency.
|
|
52
|
+
|
|
53
|
+
## Quick start
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// contracts/users.ts
|
|
57
|
+
import { z } from 'zod'
|
|
58
|
+
import { defineApiContract } from 'nuxt-api-contract/client'
|
|
59
|
+
|
|
60
|
+
export const GetUser = defineApiContract({
|
|
61
|
+
name: 'GetUser',
|
|
62
|
+
method: 'GET',
|
|
63
|
+
path: '/api/users/:id',
|
|
64
|
+
params: z.object({ id: z.string() }),
|
|
65
|
+
response: z.object({
|
|
66
|
+
id: z.string(),
|
|
67
|
+
name: z.string(),
|
|
68
|
+
email: z.string().email(),
|
|
69
|
+
}),
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// server/api/users/[id].get.ts
|
|
75
|
+
import { GetUser } from '../../../contracts/users'
|
|
76
|
+
import { createApiError, defineContractHandler } from 'nuxt-api-contract/server'
|
|
77
|
+
|
|
78
|
+
export default defineContractHandler(GetUser, async ({ params }) => {
|
|
79
|
+
const user = await db.find(params.id) // params is typed & validated
|
|
80
|
+
if (!user) throw createApiError('USER_NOT_FOUND', 'User not found', 404)
|
|
81
|
+
return user
|
|
82
|
+
})
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```vue
|
|
86
|
+
<!-- pages/users/[id].vue -->
|
|
87
|
+
<script setup lang="ts">
|
|
88
|
+
const route = useRoute()
|
|
89
|
+
const { data, error, pending } = await useApi(GetUser, {
|
|
90
|
+
params: { id: route.params.id as string },
|
|
91
|
+
})
|
|
92
|
+
// data.value?.name -> string | undefined (fully typed)
|
|
93
|
+
</script>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Passing `params: { id: 123 }` is a **TypeScript error**; an invalid request is
|
|
97
|
+
rejected at runtime with a `VALIDATION_ERROR`.
|
|
98
|
+
|
|
99
|
+
## Defining contracts
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
defineApiContract({
|
|
103
|
+
name: 'CreateUser', // optional; registers in the registry
|
|
104
|
+
version: 1, // optional metadata
|
|
105
|
+
method: 'POST', // GET | POST | PUT | PATCH | DELETE | HEAD | OPTIONS
|
|
106
|
+
path: '/api/users', // `:param` segments become required params
|
|
107
|
+
params: z.object({}), // path params schema
|
|
108
|
+
query: z.object({}), // query schema (use z.coerce for numbers)
|
|
109
|
+
body: z.object({}), // JSON body schema
|
|
110
|
+
headers: z.object({}), // raw header schema
|
|
111
|
+
response: z.object({}), // success response schema
|
|
112
|
+
errors: { EMAIL_TAKEN: z.object({}) }, // known error payloads by code
|
|
113
|
+
summary / description / tags, // OpenAPI metadata
|
|
114
|
+
auth: true, // informational / extension point
|
|
115
|
+
metadata: {}, // free-form, consumed by tooling
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Types are always **inferred** — you never write generics by hand. Path
|
|
120
|
+
parameters are extracted from the path itself: a contract for
|
|
121
|
+
`/api/posts/:postId/comments/:commentId` requires
|
|
122
|
+
`params: { postId: string, commentId: string }` even without a params schema.
|
|
123
|
+
|
|
124
|
+
### Query values
|
|
125
|
+
|
|
126
|
+
The browser sends query values as strings. Use `z.coerce.number()` (and
|
|
127
|
+
friends) for numeric query parameters; validation always happens on the server.
|
|
128
|
+
|
|
129
|
+
## Server handlers
|
|
130
|
+
|
|
131
|
+
`defineContractHandler(contract, handler)` performs, in order:
|
|
132
|
+
|
|
133
|
+
1. validation of `headers`, `params`, `query`, `body` against the schemas;
|
|
134
|
+
2. invocation of your handler with **validated, typed** data + the raw `H3Event`;
|
|
135
|
+
3. optional response validation (see configuration);
|
|
136
|
+
4. conversion of thrown `ApiError`s into the unified error payload.
|
|
137
|
+
|
|
138
|
+
Handler context:
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
interface ContractHandlerContext {
|
|
142
|
+
params // validated path params
|
|
143
|
+
query // validated query
|
|
144
|
+
body // validated body (undefined for GET/HEAD without body schema)
|
|
145
|
+
headers // validated headers (raw record when no schema)
|
|
146
|
+
event // H3Event
|
|
147
|
+
user? // reserved for auth integrations
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Client usage
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// Reactive (SSR-aware, no hydration mismatch):
|
|
155
|
+
const { data, error, pending, refresh } = await useApi(GetUser, { params: { id } })
|
|
156
|
+
|
|
157
|
+
// Imperative (actions, Pinia, event handlers):
|
|
158
|
+
const api = await useApiClient()
|
|
159
|
+
const user = await api.request(GetUser, { params: { id } }) // throws ApiError
|
|
160
|
+
const { data, error } = await api.tryRequest(GetUser, { params: { id } })
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Both work in the browser, during SSR and after hydration. During SSR the
|
|
164
|
+
request is executed **inside Nitro** (`event.$fetch`) — no HTTP round-trip to
|
|
165
|
+
itself; the payload is transferred to the client automatically.
|
|
166
|
+
|
|
167
|
+
## Error handling
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
throw createApiError('USER_NOT_FOUND', 'User not found', 404)
|
|
171
|
+
// or
|
|
172
|
+
throw createApiError({ code: 'VALIDATION_ERROR', statusCode: 400, details })
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Wire format:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{ "error": { "code": "USER_NOT_FOUND", "message": "User not found" } }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
On the client, `error` (from `useApi`) or the caught value (from
|
|
182
|
+
`useApiClient().request`) is a reconstructed `ApiError` with `code`,
|
|
183
|
+
`statusCode`, `details` and `issues`. Check it with `isApiError(value)`.
|
|
184
|
+
|
|
185
|
+
Validation errors are readable:
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
[nuxt-api-contract]
|
|
189
|
+
|
|
190
|
+
Invalid query for GET /api/users
|
|
191
|
+
|
|
192
|
+
query.limit:
|
|
193
|
+
Expected number
|
|
194
|
+
Received string
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
In production, `received` values are stripped (they may contain passwords or
|
|
198
|
+
tokens) and internal error messages are never leaked.
|
|
199
|
+
|
|
200
|
+
## SSR
|
|
201
|
+
|
|
202
|
+
Handled automatically by the transport layer:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
Browser → HTTP $fetch
|
|
206
|
+
SSR → internal Nitro call (event.$fetch)
|
|
207
|
+
After hydration → payload from useAsyncData, no refetch, no mismatch
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## OpenAPI
|
|
211
|
+
|
|
212
|
+
Configure the module (build-time generation from a contracts entry file):
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
apiContract: {
|
|
216
|
+
openapi: {
|
|
217
|
+
enabled: true,
|
|
218
|
+
entry: 'contracts/index.ts', // default array or named exports
|
|
219
|
+
path: '/_api-contracts/openapi.json',
|
|
220
|
+
title: 'My API',
|
|
221
|
+
},
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Or from the CLI (works without a Nuxt build):
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
npx nuxt-api-contract openapi contracts/index.ts --output openapi.json
|
|
229
|
+
npx nuxt-api-contract openapi contracts/index.ts --output openapi.yaml
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Zod → OpenAPI conversion is a separate abstraction layer
|
|
233
|
+
(`nuxt-api-contract/openapi`). Unsupported Zod features (transform, refine,
|
|
234
|
+
preprocess) degrade to the closest representable schema with a warning —
|
|
235
|
+
generation never fails.
|
|
236
|
+
|
|
237
|
+
## Mocking
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
import { mockContract } from 'nuxt-api-contract/client'
|
|
241
|
+
|
|
242
|
+
mockContract(GetUser, { response: () => ({ id: '1', name: 'Mocked User' }) })
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Enable `apiContract: { mocks: true }` and contract handlers return the mock
|
|
246
|
+
response (still validated against the response schema).
|
|
247
|
+
|
|
248
|
+
## Testing
|
|
249
|
+
|
|
250
|
+
Full pipeline without an HTTP server:
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
import { callContract } from 'nuxt-api-contract/testing'
|
|
254
|
+
|
|
255
|
+
const { data, error } = await callContract(GetUser, handler, { params: { id: '1' } })
|
|
256
|
+
expect(error).toBeNull()
|
|
257
|
+
expect(data.id).toBe('1')
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Validation → handler → response-validation run exactly like in production.
|
|
261
|
+
See `test/integration/playground.test.ts` for full-stack tests with
|
|
262
|
+
`@nuxt/test-utils`.
|
|
263
|
+
|
|
264
|
+
## DevTools
|
|
265
|
+
|
|
266
|
+
When `apiContract.devtools` is enabled in development, a panel lists all
|
|
267
|
+
contracts (method, path, params, tags, error codes) and includes a
|
|
268
|
+
"Try request" form. DevTools is optional — the module works normally without
|
|
269
|
+
it. `@nuxt/devtools-kit` is imported dynamically and guarded.
|
|
270
|
+
|
|
271
|
+
## Configuration
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
apiContract: {
|
|
275
|
+
validateResponse: 'development', // 'never' | 'development' | 'always'
|
|
276
|
+
mocks: false,
|
|
277
|
+
devtools: true,
|
|
278
|
+
contractsDirs: ['contracts', 'server/contracts'], // contract auto-import dirs
|
|
279
|
+
openapi: {
|
|
280
|
+
enabled: false,
|
|
281
|
+
path: '/_api-contracts/openapi.json',
|
|
282
|
+
entry: 'contracts/index.ts',
|
|
283
|
+
title: undefined, version: undefined, description: undefined,
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Runtime config (private, never in `public`):
|
|
289
|
+
|
|
290
|
+
```json
|
|
291
|
+
{ "apiContract": { "validateResponse": "development", "mocks": false } }
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Architecture
|
|
295
|
+
|
|
296
|
+
```text
|
|
297
|
+
src/
|
|
298
|
+
├── module.ts # Nuxt module (build-time)
|
|
299
|
+
├── cli.ts # `nuxt-api-contract openapi` CLI
|
|
300
|
+
├── openapi/ # Zod -> OpenAPI (isolated, build-time only)
|
|
301
|
+
├── client.ts # client-safe barrel (contracts, errors, helpers)
|
|
302
|
+
├── composables.ts # useApi / useApiClient (requires Nuxt context)
|
|
303
|
+
├── server.ts # server barrel (handler, validation)
|
|
304
|
+
├── testing.ts # callContract test helper
|
|
305
|
+
└── runtime/
|
|
306
|
+
├── shared/ # contract, types, errors, format, serialization
|
|
307
|
+
├── client/ # transport, useApi
|
|
308
|
+
└── server/ # defineContractHandler, validation, routes
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Server-only code never reaches the client bundle; the OpenAPI generator, CLI
|
|
312
|
+
and DevTools are not part of any runtime import chain (importing
|
|
313
|
+
`useApi` adds ~4 kB). See [docs/architecture.md](docs/architecture.md).
|
|
314
|
+
|
|
315
|
+
## Package exports
|
|
316
|
+
|
|
317
|
+
| Export | Purpose |
|
|
318
|
+
| --- | --- |
|
|
319
|
+
| `nuxt-api-contract` | Module definition (for `modules: []`) |
|
|
320
|
+
| `nuxt-api-contract/client` | Client-safe: `defineApiContract`, `createApiError`, registry, mocks |
|
|
321
|
+
| `nuxt-api-contract/composables` | `useApi`, `useApiClient` (Nuxt context required) |
|
|
322
|
+
| `nuxt-api-contract/server` | `defineContractHandler`, validation helpers |
|
|
323
|
+
| `nuxt-api-contract/testing` | `callContract` |
|
|
324
|
+
| `nuxt-api-contract/openapi` | OpenAPI generator |
|
|
325
|
+
| `nuxt-api-contract/shared` | Shared primitives |
|
|
326
|
+
|
|
327
|
+
## Limitations
|
|
328
|
+
|
|
329
|
+
- Zod 3.x only (`^3.23`); Zod 4 support is on the roadmap.
|
|
330
|
+
- Request bodies are JSON; `multipart/form-data` (file uploads) is planned —
|
|
331
|
+
the contract abstraction already does not assume JSON-only bodies.
|
|
332
|
+
- OpenAPI conversion is best-effort for `transform` / `refine` / `preprocess`.
|
|
333
|
+
- The standalone mock server (`nuxt-api-contract mock`) is not implemented yet.
|
|
334
|
+
- Auto-discovery is directory-based (`contracts/`, `server/contracts/`) rather
|
|
335
|
+
than a build-time scanner.
|
|
336
|
+
|
|
337
|
+
## Roadmap
|
|
338
|
+
|
|
339
|
+
See [ROADMAP.md](ROADMAP.md). In short: **0.1.0 (released)** ships core
|
|
340
|
+
contracts, OpenAPI generation and the DevTools panel; next: 0.2.0 standalone
|
|
341
|
+
mock server, 0.3.0 extended contract testing, 0.4.0 OpenAPI client generation,
|
|
342
|
+
0.5.0 external API contracts, 0.6.0 contract versioning, 1.0.0 stable API.
|
|
343
|
+
|
|
344
|
+
## Development
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
npm run build # build the package (unbuild)
|
|
348
|
+
npm run test # unit + integration tests
|
|
349
|
+
npm run test:type # type tests (vitest typecheck)
|
|
350
|
+
npm run typecheck # tsc --noEmit
|
|
351
|
+
npm run lint # eslint
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
## Publishing & Versioning
|
|
355
|
+
|
|
356
|
+
Versioning follows SemVer with the usual 0.x semantics:
|
|
357
|
+
|
|
358
|
+
- **0.x.y** (current): `y` (patch) — bug fixes; `x` (minor) — features **and**
|
|
359
|
+
documented breaking changes (pre-1.0 policy, always listed in CHANGELOG.md).
|
|
360
|
+
- **1.0.0+**: strict SemVer — breaking changes only in major releases.
|
|
361
|
+
|
|
362
|
+
Release flow:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
# 1. Bump the version (updates package.json, creates a git tag):
|
|
366
|
+
npm version patch # or: minor | major | prerelease --preid=rc
|
|
367
|
+
|
|
368
|
+
# 2. Push with tags:
|
|
369
|
+
git push --follow-tags
|
|
370
|
+
|
|
371
|
+
# 3. Publish (prepublishOnly runs lint + typecheck + unit/type tests + build):
|
|
372
|
+
npm publish
|
|
373
|
+
|
|
374
|
+
# Pre-release dist-tag (e.g. 0.2.0-rc.1):
|
|
375
|
+
npm publish --tag next
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
The package name `nuxt-api-contract` is published unscoped with public access.
|
|
379
|
+
`npm pack --dry-run` shows exactly what ships: `dist/**` (bundled entries +
|
|
380
|
+
`dist/runtime` for Nitro routes) plus README/LICENSE/CHANGELOG — no sources,
|
|
381
|
+
tests or playground.
|
|
382
|
+
|
|
383
|
+
## License
|
|
384
|
+
|
|
385
|
+
[MIT](./LICENSE) © modeusweb
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createJiti } from 'jiti';
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { resolve, dirname } from 'node:path';
|
|
5
|
+
import { generateOpenApiDocument, pickContracts } from './openapi.mjs';
|
|
6
|
+
|
|
7
|
+
function parseArgs(argv) {
|
|
8
|
+
const positional = [];
|
|
9
|
+
const flags = {};
|
|
10
|
+
let command;
|
|
11
|
+
for (let i = 0; i < argv.length; i++) {
|
|
12
|
+
const arg = argv[i];
|
|
13
|
+
if (arg.startsWith("--")) {
|
|
14
|
+
const key = arg.slice(2);
|
|
15
|
+
const next = argv[i + 1];
|
|
16
|
+
if (next && !next.startsWith("--")) {
|
|
17
|
+
flags[key] = next;
|
|
18
|
+
i++;
|
|
19
|
+
} else {
|
|
20
|
+
flags[key] = true;
|
|
21
|
+
}
|
|
22
|
+
} else if (!command) {
|
|
23
|
+
command = arg;
|
|
24
|
+
} else {
|
|
25
|
+
positional.push(arg);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { command, positional, flags };
|
|
29
|
+
}
|
|
30
|
+
async function loadContractsFromEntry(entry) {
|
|
31
|
+
const jiti = createJiti(import.meta.url, { interopDefault: true });
|
|
32
|
+
const loaded = await jiti.import(entry);
|
|
33
|
+
const values = [];
|
|
34
|
+
if (Array.isArray(loaded)) {
|
|
35
|
+
values.push(...loaded);
|
|
36
|
+
} else if (loaded !== null && typeof loaded === "object") {
|
|
37
|
+
values.push(...Object.values(loaded));
|
|
38
|
+
} else {
|
|
39
|
+
throw new Error(`[nuxt-api-contract] Entry "${entry}" must export an array of contracts or named contract exports.`);
|
|
40
|
+
}
|
|
41
|
+
const contracts = pickContracts(values);
|
|
42
|
+
if (contracts.length === 0) {
|
|
43
|
+
throw new Error(`[nuxt-api-contract] No contracts (kind === 'api-contract') found in "${entry}".`);
|
|
44
|
+
}
|
|
45
|
+
return contracts;
|
|
46
|
+
}
|
|
47
|
+
async function main() {
|
|
48
|
+
const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
49
|
+
if (command === "openapi") {
|
|
50
|
+
const entry = positional[0];
|
|
51
|
+
if (!entry || !existsSync(entry)) {
|
|
52
|
+
console.error("[nuxt-api-contract] Usage: nuxt-api-contract openapi <entry> [--output openapi.json]");
|
|
53
|
+
process.exitCode = 1;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const contracts = await loadContractsFromEntry(resolve(entry));
|
|
57
|
+
const { document, warnings } = generateOpenApiDocument(contracts, {
|
|
58
|
+
title: typeof flags.title === "string" ? flags.title : void 0,
|
|
59
|
+
version: typeof flags.version === "string" ? flags.version : void 0
|
|
60
|
+
});
|
|
61
|
+
for (const warning of warnings) {
|
|
62
|
+
console.warn(`[nuxt-api-contract] OpenAPI warning (${warning.contract}): ${warning.message}`);
|
|
63
|
+
}
|
|
64
|
+
const output = typeof flags.output === "string" ? flags.output : flags.yaml === true ? "openapi.yaml" : "openapi.json";
|
|
65
|
+
const content = flags.yaml === true ? toMinimalYaml(document) : `${JSON.stringify(document, null, 2)}
|
|
66
|
+
`;
|
|
67
|
+
mkdirSync(dirname(resolve(output)), { recursive: true });
|
|
68
|
+
writeFileSync(resolve(output), content, "utf8");
|
|
69
|
+
console.log(`[nuxt-api-contract] OpenAPI document with ${contracts.length} contract(s) written to ${output}`);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.error(`[nuxt-api-contract] Unknown command "${command ?? ""}". Available commands: openapi`);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
}
|
|
75
|
+
function toMinimalYaml(value, indent = 0) {
|
|
76
|
+
const pad = " ".repeat(indent);
|
|
77
|
+
if (Array.isArray(value)) {
|
|
78
|
+
if (value.length === 0) return "[]";
|
|
79
|
+
return value.map((item) => `${pad}- ${toMinimalYaml(item, indent + 2).trimStart()}`).join("\n");
|
|
80
|
+
}
|
|
81
|
+
if (value !== null && typeof value === "object") {
|
|
82
|
+
const entries = Object.entries(value);
|
|
83
|
+
if (entries.length === 0) return "{}";
|
|
84
|
+
return entries.map(([key, item]) => {
|
|
85
|
+
const rendered = toMinimalYaml(item, indent + 2);
|
|
86
|
+
if (item !== null && typeof item === "object" && (Array.isArray(item) && item.length > 0 || Object.keys(item).length > 0)) {
|
|
87
|
+
return `${pad}${key}:
|
|
88
|
+
${rendered}`;
|
|
89
|
+
}
|
|
90
|
+
return `${pad}${key}: ${rendered.trimStart()}`;
|
|
91
|
+
}).join("\n");
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
94
|
+
return String(value);
|
|
95
|
+
}
|
|
96
|
+
main().catch((error) => {
|
|
97
|
+
console.error(error instanceof Error ? error.message : error);
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export { A as ApiError, a as ApiErrorPayload, B as BUILT_IN_ERROR_CODES, C as CreateApiErrorInput, V as ValidationIssue, c as createApiError, f as formatValidationMessage, i as isApiError, p as parseApiErrorPayload, s as sanitizeIssues, b as serializeApiError, t as toApiError, d as toValidationIssues } from './shared/nuxt-api-contract.D31EDcwH.mjs';
|
|
2
|
+
import { A as AnyApiContract, M as MaybePromise, i as ContractHandlerResponse, c as ApiContractDefinition, h as ContractFromDefinition } from './shared/nuxt-api-contract.CPm9WbWA.mjs';
|
|
3
|
+
export { a as API_CONTRACT_KIND, b as ApiContract, d as ApiRequestOptions, e as AuthConfig, C as ContractBodyInput, f as ContractClientResponse, g as ContractErrorCode, j as ContractHeadersInput, k as ContractParamsInput, l as ContractPathParams, m as ContractQueryInput, E as EmptyObject, n as ExtractSchemaInput, o as ExtractSchemaOutput, H as HttpMethod, I as IsEmptyObject, P as PathParams, R as ResolvedApiRequestOptions, S as SplitPath } from './shared/nuxt-api-contract.CPm9WbWA.mjs';
|
|
4
|
+
import * as zod from 'zod';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registers a named contract. Called automatically by `defineApiContract`
|
|
8
|
+
* when a `name` is provided. Duplicate names are overwritten with a warning.
|
|
9
|
+
*/
|
|
10
|
+
declare function registerContract(contract: AnyApiContract): void;
|
|
11
|
+
declare function getContractByName(name: string): AnyApiContract | undefined;
|
|
12
|
+
declare function listRegisteredContracts(): AnyApiContract[];
|
|
13
|
+
/** Test helper: clears the registry. */
|
|
14
|
+
declare function clearContractRegistry(): void;
|
|
15
|
+
interface ContractMock<C extends AnyApiContract> {
|
|
16
|
+
/** Factory producing a (validated) response for the contract. */
|
|
17
|
+
response?: () => MaybePromise<MockResponseInput<C>>;
|
|
18
|
+
/** Simulated latency in ms. */
|
|
19
|
+
delay?: number;
|
|
20
|
+
}
|
|
21
|
+
type MockResponseInput<C extends AnyApiContract> = C['response'] extends zod.ZodType ? ContractHandlerResponse<C> : unknown;
|
|
22
|
+
/**
|
|
23
|
+
* Registers a mock implementation for a contract. When the module option
|
|
24
|
+
* `apiContract.mocks` is enabled, contract handlers return the mock response
|
|
25
|
+
* (still validated against the response schema).
|
|
26
|
+
*/
|
|
27
|
+
declare function mockContract<C extends AnyApiContract>(contract: C, mock: ContractMock<C>): void;
|
|
28
|
+
declare function getContractMock<C extends AnyApiContract>(contract: C): ContractMock<C> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Defines a type-safe API contract.
|
|
31
|
+
*
|
|
32
|
+
* The contract is the single source of truth for runtime validation,
|
|
33
|
+
* TypeScript types, OpenAPI generation, DevTools and mocks.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* export const GetUser = defineApiContract({
|
|
38
|
+
* method: 'GET',
|
|
39
|
+
* path: '/api/users/:id',
|
|
40
|
+
* params: z.object({ id: z.string().uuid() }),
|
|
41
|
+
* response: z.object({ id: z.string(), name: z.string() }),
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare function defineApiContract<const TDef extends ApiContractDefinition>(definition: TDef): ContractFromDefinition<TDef>;
|
|
46
|
+
/** Type guard for contract objects. */
|
|
47
|
+
declare function isApiContract(value: unknown): value is AnyApiContract;
|
|
48
|
+
/**
|
|
49
|
+
* Builds the request URL from a contract path and concrete params.
|
|
50
|
+
* Remaining params that are not part of the path are ignored.
|
|
51
|
+
*/
|
|
52
|
+
declare function buildRequestPath(path: string, params: Record<string, unknown> | undefined): string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Serialization helpers shared by the client transport and cache keys.
|
|
56
|
+
*
|
|
57
|
+
* The protocol intentionally stays minimal: standard JSON plus two common
|
|
58
|
+
* edge cases (Date and bigint). Response serialization relies on Nitro's
|
|
59
|
+
* built-in devalue support, which already handles Date, RegExp, etc.
|
|
60
|
+
*/
|
|
61
|
+
/** Converts query values into URL-safe primitives (Date -> ISO, bigint -> string). */
|
|
62
|
+
type SerializedQueryValue = string | number | boolean | Array<string | number | boolean>;
|
|
63
|
+
declare function serializeQueryValue(value: unknown): SerializedQueryValue;
|
|
64
|
+
/** Prepares a query object for `$fetch` / URL building; drops `undefined` entries. */
|
|
65
|
+
declare function serializeQuery(query: Record<string, unknown> | undefined): Record<string, SerializedQueryValue> | undefined;
|
|
66
|
+
/** Deterministic JSON stringify (sorted keys) for stable SSR cache keys. */
|
|
67
|
+
declare function stableStringify(value: unknown): string;
|
|
68
|
+
|
|
69
|
+
export { AnyApiContract, ApiContractDefinition, ContractFromDefinition, ContractHandlerResponse, MaybePromise, buildRequestPath, clearContractRegistry, defineApiContract, getContractByName, getContractMock, isApiContract, listRegisteredContracts, mockContract, registerContract, serializeQuery, serializeQueryValue, stableStringify };
|
|
70
|
+
export type { ContractMock, MockResponseInput };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export { A as ApiError, a as ApiErrorPayload, B as BUILT_IN_ERROR_CODES, C as CreateApiErrorInput, V as ValidationIssue, c as createApiError, f as formatValidationMessage, i as isApiError, p as parseApiErrorPayload, s as sanitizeIssues, b as serializeApiError, t as toApiError, d as toValidationIssues } from './shared/nuxt-api-contract.D31EDcwH.js';
|
|
2
|
+
import { A as AnyApiContract, M as MaybePromise, i as ContractHandlerResponse, c as ApiContractDefinition, h as ContractFromDefinition } from './shared/nuxt-api-contract.CPm9WbWA.js';
|
|
3
|
+
export { a as API_CONTRACT_KIND, b as ApiContract, d as ApiRequestOptions, e as AuthConfig, C as ContractBodyInput, f as ContractClientResponse, g as ContractErrorCode, j as ContractHeadersInput, k as ContractParamsInput, l as ContractPathParams, m as ContractQueryInput, E as EmptyObject, n as ExtractSchemaInput, o as ExtractSchemaOutput, H as HttpMethod, I as IsEmptyObject, P as PathParams, R as ResolvedApiRequestOptions, S as SplitPath } from './shared/nuxt-api-contract.CPm9WbWA.js';
|
|
4
|
+
import * as zod from 'zod';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Registers a named contract. Called automatically by `defineApiContract`
|
|
8
|
+
* when a `name` is provided. Duplicate names are overwritten with a warning.
|
|
9
|
+
*/
|
|
10
|
+
declare function registerContract(contract: AnyApiContract): void;
|
|
11
|
+
declare function getContractByName(name: string): AnyApiContract | undefined;
|
|
12
|
+
declare function listRegisteredContracts(): AnyApiContract[];
|
|
13
|
+
/** Test helper: clears the registry. */
|
|
14
|
+
declare function clearContractRegistry(): void;
|
|
15
|
+
interface ContractMock<C extends AnyApiContract> {
|
|
16
|
+
/** Factory producing a (validated) response for the contract. */
|
|
17
|
+
response?: () => MaybePromise<MockResponseInput<C>>;
|
|
18
|
+
/** Simulated latency in ms. */
|
|
19
|
+
delay?: number;
|
|
20
|
+
}
|
|
21
|
+
type MockResponseInput<C extends AnyApiContract> = C['response'] extends zod.ZodType ? ContractHandlerResponse<C> : unknown;
|
|
22
|
+
/**
|
|
23
|
+
* Registers a mock implementation for a contract. When the module option
|
|
24
|
+
* `apiContract.mocks` is enabled, contract handlers return the mock response
|
|
25
|
+
* (still validated against the response schema).
|
|
26
|
+
*/
|
|
27
|
+
declare function mockContract<C extends AnyApiContract>(contract: C, mock: ContractMock<C>): void;
|
|
28
|
+
declare function getContractMock<C extends AnyApiContract>(contract: C): ContractMock<C> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Defines a type-safe API contract.
|
|
31
|
+
*
|
|
32
|
+
* The contract is the single source of truth for runtime validation,
|
|
33
|
+
* TypeScript types, OpenAPI generation, DevTools and mocks.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* export const GetUser = defineApiContract({
|
|
38
|
+
* method: 'GET',
|
|
39
|
+
* path: '/api/users/:id',
|
|
40
|
+
* params: z.object({ id: z.string().uuid() }),
|
|
41
|
+
* response: z.object({ id: z.string(), name: z.string() }),
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare function defineApiContract<const TDef extends ApiContractDefinition>(definition: TDef): ContractFromDefinition<TDef>;
|
|
46
|
+
/** Type guard for contract objects. */
|
|
47
|
+
declare function isApiContract(value: unknown): value is AnyApiContract;
|
|
48
|
+
/**
|
|
49
|
+
* Builds the request URL from a contract path and concrete params.
|
|
50
|
+
* Remaining params that are not part of the path are ignored.
|
|
51
|
+
*/
|
|
52
|
+
declare function buildRequestPath(path: string, params: Record<string, unknown> | undefined): string;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Serialization helpers shared by the client transport and cache keys.
|
|
56
|
+
*
|
|
57
|
+
* The protocol intentionally stays minimal: standard JSON plus two common
|
|
58
|
+
* edge cases (Date and bigint). Response serialization relies on Nitro's
|
|
59
|
+
* built-in devalue support, which already handles Date, RegExp, etc.
|
|
60
|
+
*/
|
|
61
|
+
/** Converts query values into URL-safe primitives (Date -> ISO, bigint -> string). */
|
|
62
|
+
type SerializedQueryValue = string | number | boolean | Array<string | number | boolean>;
|
|
63
|
+
declare function serializeQueryValue(value: unknown): SerializedQueryValue;
|
|
64
|
+
/** Prepares a query object for `$fetch` / URL building; drops `undefined` entries. */
|
|
65
|
+
declare function serializeQuery(query: Record<string, unknown> | undefined): Record<string, SerializedQueryValue> | undefined;
|
|
66
|
+
/** Deterministic JSON stringify (sorted keys) for stable SSR cache keys. */
|
|
67
|
+
declare function stableStringify(value: unknown): string;
|
|
68
|
+
|
|
69
|
+
export { AnyApiContract, ApiContractDefinition, ContractFromDefinition, ContractHandlerResponse, MaybePromise, buildRequestPath, clearContractRegistry, defineApiContract, getContractByName, getContractMock, isApiContract, listRegisteredContracts, mockContract, registerContract, serializeQuery, serializeQueryValue, stableStringify };
|
|
70
|
+
export type { ContractMock, MockResponseInput };
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { b as buildRequestPath, c as clearContractRegistry, d as defineApiContract, g as getContractByName, a as getContractMock, i as isApiContract, l as listRegisteredContracts, m as mockContract, r as registerContract, s as serializeQuery, e as serializeQueryValue, f as stableStringify } from './shared/nuxt-api-contract.QDGSGaVY.mjs';
|
|
2
|
+
export { A as ApiError, B as BUILT_IN_ERROR_CODES, c as createApiError, i as isApiError, p as parseApiErrorPayload, s as serializeApiError, t as toApiError } from './shared/nuxt-api-contract.obS6uV8A.mjs';
|
|
3
|
+
export { f as formatValidationMessage, s as sanitizeIssues, t as toValidationIssues } from './shared/nuxt-api-contract.DCAU2j7t.mjs';
|