OpenAPI auto generated meta from defineEventHandler<{ body: {...} }> Request types
Describe the feature
I was looking for a way to auto generate openAPI meta based of my api routes.
defineEventHandler already allows to define the Request type like:
export default defineEventHandler<{
body: { foo: string, bar: boolean },
}>(() => {});
or something like:
import { z } from 'zod';
const bodySchema = z.object({
foo: z.string(),
bar: z.boolean(),
});
export default defineEventHandler<{
body: z.infer<typeof bodySchema>,
}>(() => {});
I came across this issue: #2974
But it would be nice, to be able to define it with defineEventHandler.
Thanks!
Additional information
- [x] Would you be willing to help implement this feature?
Elysia does this very well with their router, you can define routes and validations the normal way, and after adding 1 "use swagger" line you get full documentation youve always wanted but never asked for, you can even hide routes from the documentation
https://elysiajs.com/plugins/swagger
I suggest just follow elysia on this one where youd allow users to pass validation instead of having them add unexecutable ugly unmaintainable "meta" documentation to their code
maybe nitro users would want to deal with the meta primitive, but nuxt users dont have a use for such a thing
here is an example route with elysia that generates helpful openapi (im more comfortable with zod so I convert it to typebox instead of writing typebox straight away)
const app = new Elysia();
app.use(
swagger({
documentation: {
tags: [
{
name: "client",
description: "Client-side operations for user authentication",
},
{
name: "server",
description: "Server-side operations for token management",
},
],
},
})
);
app
.group("client", (app) =>
app
.group("signup", (app) =>
app.group("email", (app) =>
app
.post(
"/",
({ body }) => {
// Handler logic for registration
},
{
body: TypeBox(
z.object({
email: z
.string({
description: "User email address",
})
.email()
.max(255),
password: z
.string({
description: "Password",
})
.min(8, "Password must be at least 8 characters")
.max(64, "Password must not exceed 64 characters")
.regex(
/^[a-zA-Z0-9!@#$%^&*()_+=-]+$/,
"Password must be alphanumeric with allowed special characters"
),
})
),
detail: "Register a new user",
tags: ["client"],
}
)
)
)
)
This is my current workaround, keeping in mind the defineRouteMeta stuff is basically compile time at the moment but you can still modify the response before it gets sent out using plugins. Seems to work with my limited testing but I haven't been able ot get it working with routeRules yet.
I have to actually bother to check the docs to make sure I remembered to "register" various schemas, and it doesn't really handle de-duping/referencing all that well.
Probably applicable for this ticket too: #2974
Using Nuxt 4, Zod v4, and @asteasolutions/zod-to-openapi v8.
(Pardon the tabs, they look better with tab-width: 2)
server/openapi-models.ts
import { beanSchema } from '#server/features/bean-counting/schemas/bean-schema'
/**
* An exported array of Zod schemas,
* to be registered as OpenAPI models
*/
export const schemas = [
beanSchema, // assuming I used .meta({ id: 'Bean', title: 'Bean' })
]
server/plugins/01.populate-openapi-schemas.ts
import { OpenApiGeneratorV31 } from '@asteasolutions/zod-to-openapi'
import { schemas } from '#server/openapi-models'
export default defineNitroPlugin((nitro) => {
nitro.hooks.hook('beforeResponse', async (event, response) => {
if (event.path === '/api/_openapi.json') {
// Can't figure out a way to cache this via route rules yet
// You'd think we could get the schemas straight from Zod, right?
// e.g. `const schemas = z.globalRegistry._map.keys().toArray()`
// But this doesn't work because we use schemas that
// *don't* exist at runtime (e.g. return values) in many cases too.
// So we need a static array of these schemas for now.
console.info('Attaching Zod schemas to OpenAPI docs')
const docJson = response.body as {
components?: {
schemas?: unknown
}
} // Apparently don't need JSON.parse(response.body as string)
docJson.components ??= {}
const generator = new OpenApiGeneratorV31(schemas)
const generated = generator.generateComponents()
docJson.components = generated.components
response.body = docJson
}
})
})
server/api/bean-counting/v1/bean.post.ts
export default defineEventHandler(async (event) => { /* logic here */ })
defineRouteMeta({
openAPI: {
tags: ['Bean Counting'],
description:
'Add a bean to the bean count',
requestBody: {
description: 'An uncounted raw bean',
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Bean',
},
},
},
},
},
responses: {
'200': {
description: 'An extremely enumerated baked bean',
content: {
'application/json': {
schema: {
type: 'array',
items: {
$ref: '#/components/schemas/Bean',
},
},
},
},
},
},
},
})