nitro icon indicating copy to clipboard operation
nitro copied to clipboard

OpenAPI auto generated meta from defineEventHandler<{ body: {...} }> Request types

Open Bombastickj opened this issue 10 months ago • 2 comments

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?

Bombastickj avatar Apr 02 '25 17:04 Bombastickj

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

MahmoodKhalil57 avatar Apr 13 '25 04:04 MahmoodKhalil57

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

Image

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"],
              }
            )
        )
      )
    )

MahmoodKhalil57 avatar Apr 13 '25 04:04 MahmoodKhalil57

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',
							},
						},
					},
				},
			},
		},
	},
})

ceigey avatar Jul 18 '25 08:07 ceigey