This module was built v3-native (no v2 migration), but every v3-specific decision and pitfall is recorded here — useful as a checklist for migrating other modules (see nitro's migration guide) or building new ones.
NitroModulefromnitro/types, object{ name, setup }withsatisfies NitroModule— nodefineNitroModule(the kit is gone in v3), no bare function (src/index.ts).- Typed options via
declare module "nitro/types": augment bothNitroConfigandNitroOptionssoopenAPISchemas: {...}is typed innitro.config.ts. - Registered by direct import:
modules: [openAPISchemas]. AllNitroModuleInputforms work (path string, object, bare setup,{ nitro }); we use the object.
- Virtual module via
nitro.options.virtual["#nitro-openapi-schemas"]with a lazy template (function) — evaluated at build time whennitro.routingis populated, same mechanism as nitro's internalrouting-metavirtual. - Scanned routes read from
nitro.routing.routes.routes— the v3 API (v2 usedscannedHandlers/options.handlers). - Routes registered via
nitro.options.handlers.push— unchanged from v2, but the Scalar handler is resolved fromruntimeDirexported bynitro/meta(new v3 subpath). - Builder-agnostic: nothing Rollup/Rolldown/Vite-specific — a v3 requirement since all three builders coexist (
NITRO_BUILDER).
- App-level code imports h3 utilities from
nitro/h3, not bareh3— the convention nitro's own docs and test fixtures use consistently (nitro/h3re-exports all ofh3, includingdefineValidatedHandler, which isn't in nitro's small curated re-export list from the barenitropackage). Demo routes (examples/nitro/routes) all follow this. - Module-internal runtime code is the exception: src/runtime/route.ts imports bare
h3directly, matching how nitro's own internals do it (nitro/src/runtime/internal/*never imports its ownnitro/h3subpath — no reason for a module's own runtime code to route through the app-facing re-export either). - Web standards only:
event.req.url/event.req.json(); neverevent.node,readBody, orgetHeader. - The generator is fully runtime-agnostic: zero Node APIs, dynamic imports for vendor converters.
| Pitfall | Fix |
|---|---|
Filesystem scanning is opt-in in v3 (serverDir: false by default) → zero routes, empty spec |
serverDir: "./" in the example's config |
Node refuses type-stripping of .ts under node_modules (package consumed without a build step) |
jiti as devDependency — c12 picks it up automatically |
workspace:* can't resolve the workspace root package with bun |
"nitro-openapi-schemas": "file:../.." in the example |
Zod 4 emits nested registered schemas as local $defs (#/$defs/Payment — invalid as OpenAPI components pointer) |
hoist $defs into components/schemas + recursively rewrite refs (src/runtime/generator.ts) |
Rolldown tries to resolve @valibot/to-json-schema (optional dep) and warns on every build |
split specifier: "@valibot/" + "to-json-schema" |
| Direct imports in the virtual module defeat handler lazy-loading | accepted and documented in the RFC as the Phase 1 trade-off, not a bug — no build-time warning is guaranteed to surface it (we saw Rolldown's INEFFECTIVE_DYNAMIC_IMPORT fire on a single-app layout but not once the module became a separate npm package, and never fully traced why) |
Zod's .meta() is a callable getter, ArkType's .meta is a plain property (.configure({ id }) sets it) — calling .meta?.() on an ArkType schema throws instead of returning undefined |
per-vendor schemaId() helper (src/runtime/generator.ts) instead of one shared accessor |
@valibot/to-json-schema's plain toJsonSchema() predates Standard JSON Schema and returns a raw JSON Schema object, not a spec-compliant wrapper — silently produces schemas but breaks named-schema hoisting since there's no ~standard to read |
use toStandardJsonSchema() (1.5+) instead, and read v.metadata({ id }) off the raw schema's pipe before wrapping, since the wrapper strips everything down to ~standard |
- Layout:
src/index.ts+src/runtime/+examples/nitro/; framework deps as peerDependencies (h3 ^2.0.1-rc.22— the exact rc nitro pins, so package managers dedupe to one copy). - Schema libraries (
zod,arktype,@valibot/to-json-schema) are optional peer dependencies too (peerDependenciesMeta.optional), with the version floors Standard JSON Schema actually requires (Zod 4.2+, ArkType 2.1.28+,@valibot/to-json-schema1.5+) — they were reached only through untyped dynamic imports before, with no install-time signal of what's supported. - Deliberate deviation: no unbuild/dist —
exportspoints atsrc/index.tsdirectly. Fine until npm publishing.
- unbuild (or obuild) +
files: ["dist"]+exportspointing atdist/ - optional
{ nitro: ... }entry for Nuxt dual-packaging - track h3's
validate.response(proposed in the RFC) to replacemeta.openAPI.responses[].schema