@codewithagents/openapi-server 1.9.0 → 1.10.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/README.md CHANGED
@@ -216,7 +216,8 @@ See the [full configuration reference](https://openapi.codewithagents.de/openapi
216
216
  "input_openapi": "./spec/api.json", // required: path to OpenAPI 3.x spec (JSON or YAML)
217
217
  "output": "./generated", // required: directory to write generated files
218
218
  "framework": "hono", // optional: router target (default: "none")
219
- "input_schema": "./generated/schemas.ts" // optional: Zod schema file for request validation
219
+ "input_schema": "./generated/schemas.ts", // optional: Zod schema file for request validation
220
+ "context_type": "RequestContext" // optional: TypeScript type for per-request caller context
220
221
  }
221
222
  ```
222
223
 
@@ -226,6 +227,7 @@ See the [full configuration reference](https://openapi.codewithagents.de/openapi
226
227
  | `output` | Yes | n/a | Directory to write `service.ts` and `router.ts` |
227
228
  | `framework` | No | `"none"` | Router framework to generate: `"hono"`, `"express"`, `"fastify"`, or `"none"`. Use `"none"` to generate only `service.ts` |
228
229
  | `input_schema` | No | none | Path to user-owned Zod schema file. Enables server-side request validation (see below) |
230
+ | `context_type` | No | none | TypeScript type name to thread through service methods as a final `ctx` argument. See below. |
229
231
 
230
232
  Use `--config <path>` to point at a config file in a different location:
231
233
 
@@ -315,6 +317,241 @@ fastify.register(async (instance) => { createRouter(instance, service) }, { pref
315
317
 
316
318
  The `"none"` path is always available and keeps the zero-footprint promise: the generated code has no runtime dependencies that you did not already choose.
317
319
 
320
+ ## Cookie parameter validation
321
+
322
+ Operations that declare `in: cookie` parameters get the same Zod validation treatment as header and query params. The generator reads the cookie name and schema constraints (required, enum, minLength, maxLength, pattern) from the spec and emits a `_ckv` safeParse block in the generated route handler. Failures return `422 { error: 'Invalid request cookies', issues }`.
323
+
324
+ Cookie names are case-sensitive (unlike HTTP headers, which are always lowercased before lookup). The exact name from the spec is used for both the Zod field key and the value lookup.
325
+
326
+ Cookies are not forwarded to the service method signature. They are validated in the router layer only. Forwarding cookies to service methods is out of scope for the current release.
327
+
328
+ **Per-framework plugin requirements:**
329
+
330
+ | Framework | Required plugin / middleware | Cookie accessor |
331
+ |---|---|---|
332
+ | Fastify | `@fastify/cookie` registered before the router | `req.cookies['name']` |
333
+ | Express | `cookie-parser` middleware applied before mounting the router | `req.cookies['name']` |
334
+ | Hono | `hono/cookie` (imported automatically in the generated output) | `getCookie(c, 'name')` |
335
+
336
+ **Fastify setup:**
337
+
338
+ ```ts
339
+ import fastifyCookie from '@fastify/cookie'
340
+
341
+ fastify.register(fastifyCookie)
342
+ fastify.register(async (instance) => { createRouter(instance, service) }, { prefix: '/api' })
343
+ ```
344
+
345
+ **Express setup:**
346
+
347
+ ```ts
348
+ import cookieParser from 'cookie-parser'
349
+
350
+ app.use(cookieParser())
351
+ app.use('/api', createRouter(service))
352
+ ```
353
+
354
+ **Hono setup:**
355
+
356
+ No extra setup needed. The generator automatically adds `import { getCookie } from 'hono/cookie'` to the generated router when the spec declares cookie params. `hono/cookie` ships with Hono; no additional install is required.
357
+
358
+ ---
359
+
318
360
  ## Error handling and troubleshooting
319
361
 
320
- The generated router does not wrap service calls in `try/catch`. Errors propagate to the framework's own error handler. See [Error handling](https://openapi.codewithagents.de/openapi-server#error-handling) in the docs for per-framework error handler examples and [Troubleshooting](https://openapi.codewithagents.de/openapi-server#troubleshooting) for common issues such as missing Zod validation or `Cannot find module './models.js'`.
362
+ The generated router wraps every service call in a `try/catch` block. The catch block handles two cases:
363
+
364
+ - **`HttpError`** (exported from the generated `router.ts`): caught inline and mapped to its `.status` code. Use `new HttpError(404, 'Pet not found')` inside service methods to return structured HTTP error responses.
365
+ - **All other errors**: re-thrown to the framework's own error handler (`setErrorHandler` in Fastify, error-handling middleware in Express, `app.onError` in Hono).
366
+
367
+ This means custom error types that do NOT extend `HttpError` propagate to the framework layer, where you install a single error handler for logging, monitoring, and response shaping.
368
+
369
+ **Example: custom error reaching Fastify's `setErrorHandler`**
370
+
371
+ ```ts
372
+ // Your custom error class — does NOT extend HttpError
373
+ class NotFoundError extends Error {
374
+ constructor(resource: string) {
375
+ super(`${resource} not found`)
376
+ this.name = 'NotFoundError'
377
+ }
378
+ }
379
+
380
+ // Service implementation throws NotFoundError
381
+ export const petService: PetstoreService = {
382
+ async getPet(id) {
383
+ const pet = db.get(id)
384
+ if (!pet) throw new NotFoundError(`Pet ${id}`)
385
+ return pet
386
+ },
387
+ // ...
388
+ }
389
+
390
+ // Register a Fastify error handler ONCE at the app level.
391
+ // The generated router re-throws non-HttpError errors, so they arrive here.
392
+ fastify.setErrorHandler((err, request, reply) => {
393
+ if (err.name === 'NotFoundError') {
394
+ return reply.status(404).send({ error: err.message })
395
+ }
396
+ // Unknown errors become 500
397
+ fastify.log.error(err)
398
+ return reply.status(500).send({ error: 'Internal server error' })
399
+ })
400
+
401
+ fastify.register(async (instance) => { createRouter(instance, petService) }, { prefix: '/api' })
402
+ ```
403
+
404
+ The same pattern applies to Express error middleware (`app.use((err, req, res, next) => { ... })`) and to Hono's `app.onError((err, c) => { ... })`.
405
+
406
+ See [Error handling](https://openapi.codewithagents.de/openapi-server#error-handling) in the docs for per-framework error handler examples and [Troubleshooting](https://openapi.codewithagents.de/openapi-server#troubleshooting) for common issues such as missing Zod validation or `Cannot find module './models.js'`.
407
+
408
+ ---
409
+
410
+ ## Request-scoped context / caller principal (`context_type`)
411
+
412
+ The `context_type` config option threads a typed caller context through every generated service method. Use it to pass an authentication principal, a tenant ID, or any per-request metadata without coupling service code to framework types.
413
+
414
+ **Config:**
415
+
416
+ ```json
417
+ {
418
+ "input_openapi": "./spec/api.json",
419
+ "output": "./generated",
420
+ "framework": "hono",
421
+ "context_type": "RequestContext"
422
+ }
423
+ ```
424
+
425
+ **What changes in generated `service.ts`:**
426
+
427
+ ```ts
428
+ // Without context_type (default):
429
+ export interface PetstoreService {
430
+ listPets(params?: { species?: string }): Promise<Pet[]>
431
+ getPet(id: string): Promise<Pet>
432
+ }
433
+
434
+ // With context_type: "RequestContext":
435
+ export interface PetstoreService<Ctx = never> {
436
+ listPets(params?: { species?: string }, ctx: Ctx): Promise<Pet[]>
437
+ getPet(id: string, ctx: Ctx): Promise<Pet>
438
+ }
439
+ ```
440
+
441
+ The generic default `Ctx = never` keeps the interface usable when no context is needed: existing implementations that do not pass ctx continue to compile as long as the service is instantiated without a type argument.
442
+
443
+ **What changes in generated `router.ts`:**
444
+
445
+ The generated router passes the framework's native request/context object as the final argument to every service call:
446
+
447
+ | Framework | ctx value passed |
448
+ |---|---|
449
+ | Hono | `c` (the Hono `Context` object) |
450
+ | Express | `req` (the Express `Request` object) |
451
+ | Fastify | `req` (the Fastify `FastifyRequest` object) |
452
+
453
+ ```ts
454
+ // Generated Hono handler (with context_type: "RequestContext"):
455
+ app.get('/pets', async (c) => {
456
+ try {
457
+ return c.json(await service.listPets(c))
458
+ } catch (err) { ... }
459
+ })
460
+ ```
461
+
462
+ **Implementing the service with context:**
463
+
464
+ ```ts
465
+ import type { PetstoreService } from '../generated/service.js'
466
+ import type { Context } from 'hono'
467
+
468
+ // Define your request context type
469
+ interface RequestContext {
470
+ userId: string
471
+ tenantId: string
472
+ }
473
+
474
+ // Extract context from the Hono Context object in a middleware
475
+ const app = new Hono()
476
+ app.use('*', async (c, next) => {
477
+ const userId = c.req.header('x-user-id') ?? ''
478
+ // Store on the Hono context so the generated router can pass it
479
+ c.set('userId', userId)
480
+ await next()
481
+ })
482
+
483
+ // Implement the service — ctx is whatever the router passed (here: the Hono Context)
484
+ export const petService: PetstoreService<Context> = {
485
+ async listPets(params, ctx) {
486
+ const userId = ctx.get('userId')
487
+ return db.listPets({ userId, ...params })
488
+ },
489
+ async getPet(id, ctx) {
490
+ const userId = ctx.get('userId')
491
+ return db.getPet(id, userId)
492
+ },
493
+ }
494
+ ```
495
+
496
+ **Backward compatibility:** if `context_type` is not set in the config, the generated output is identical to previous versions. No changes to the interface shape, no extra arguments in service calls.
497
+
498
+ ---
499
+
500
+ ## Non-JSON request bodies (Fastify)
501
+
502
+ Fastify 5 natively parses only `application/json` and `text/plain` request bodies. For other content types you must register the appropriate plugin before the generated router.
503
+
504
+ ### application/x-www-form-urlencoded
505
+
506
+ Install and register [`@fastify/formbody`](https://github.com/fastify/fastify-formbody):
507
+
508
+ ```bash
509
+ pnpm add @fastify/formbody
510
+ ```
511
+
512
+ ```ts
513
+ import fastifyFormbody from '@fastify/formbody'
514
+
515
+ fastify.register(fastifyFormbody)
516
+ fastify.register(async (instance) => { createRouter(instance, service) }, { prefix: '/api' })
517
+ ```
518
+
519
+ Without this plugin, `req.body` is `undefined` for form-urlencoded requests and the handler receives no body.
520
+
521
+ ### multipart/form-data
522
+
523
+ Install and register [`@fastify/multipart`](https://github.com/fastify/fastify-multipart) with `attachFieldsToBody: true`:
524
+
525
+ ```bash
526
+ pnpm add @fastify/multipart
527
+ ```
528
+
529
+ ```ts
530
+ import fastifyMultipart from '@fastify/multipart'
531
+
532
+ fastify.register(fastifyMultipart, { attachFieldsToBody: true })
533
+ fastify.register(async (instance) => { createRouter(instance, service) }, { prefix: '/api' })
534
+ ```
535
+
536
+ The `attachFieldsToBody` option is required. Without it, `@fastify/multipart` v10 exposes uploaded files only via async iterators (`request.parts()`), not via `req.body`. The generated router reads `req.body` and passes it to the service method, so `attachFieldsToBody: true` must be set.
537
+
538
+ ### application/octet-stream
539
+
540
+ No extra plugin is needed. When your spec declares an `application/octet-stream` request body, the generator automatically emits an `addContentTypeParser` call inside `createRouter`:
541
+
542
+ ```ts
543
+ app.addContentTypeParser('application/octet-stream', { parseAs: 'buffer' }, (req, body, done) => done(null, body))
544
+ ```
545
+
546
+ `addContentTypeParser` is a core Fastify API with no additional dependencies. The parsed body is a `Buffer` and is forwarded directly to the service method.
547
+
548
+ ### 415 error-shape divergence
549
+
550
+ When a request arrives with an unsupported content type and no parser is registered, the two frameworks return different shapes:
551
+
552
+ | Framework | Status | Body shape |
553
+ |---|---|---|
554
+ | Hono | 415 | `{ error: 'Unsupported Media Type' }` |
555
+ | Fastify | 415 | `{ statusCode: 415, code: 'FST_ERR_CTP_INVALID_MEDIA_TYPE', error: 'Unsupported Media Type', message: '...' }` |
556
+
557
+ Hono uses the shared `{ error }` envelope from the generated router. Fastify uses its own framework-level 415 envelope, which is emitted before the route handler runs. If you rely on a consistent error shape across frameworks, register the appropriate parser or add a Fastify `setErrorHandler` that normalises the response.