log
Swift Code Chronicles

Nuxt3 Best Practices 03: OpenAPI Documentation Output

Published on February 2, 2025
Updated on February 2, 2025
32 min read
Vue

1. Introduction

In Nuxt 3, we can leverage Nitro’s built-in OpenAPI documentation generation feature and visualize it using Swagger UI or Scalar UI. However, this feature has the following issues:

  1. Experimental feature: Nitro’s OpenAPI support is still in the experimental stage and may be modified or removed in the future.
  2. Lack of API filtering mechanism: All Server APIs are exposed, with no configuration option to exclude specific APIs.
  3. Incompatibility with Zod: If using zod to define schemas, Nitro cannot automatically convert them to OpenAPI format, requiring additional JSON Schema definitions.

Therefore, I recommend a more flexible approach with the following advantages:

  • Seamless integration with zod, allowing direct generation of OpenAPI documentation from zod schemas.
  • Support for API filtering, enabling selective hiding of specific APIs.

2. Implementation Plan

2.1 Install Dependencies

First, we need to install zod and its OpenAPI adapter library zod-openapi.

npm install zod zod-openapi

2.2 Overview of the Approach

The main implementation steps are as follows:

  1. Create an OpenAPI registry: Used to collect API definitions.
  2. Provide a defineOpenAPI method: Allows each API to register its interface information with the registry.
  3. Define an OpenAPI generation API: Reads registry data and generates an OpenAPI JSON document.

2.3 Create an OpenAPI Registry

Create openapi-registry.ts in the server/utils directory to store and manage API definitions.

// openapi-registry.ts
import z from 'zod';
import { extendZodWithOpenApi } from 'zod-openapi';

extendZodWithOpenApi(z);

export type OpenAPIEndpoint = {
  /** API tags */
  tags: string[];
  /** API path, e.g., '/api/users' */
  path: string;
  /** Request method, e.g., 'get' or 'post' */
  method: string;
  /** API operation ID */
  operationId?: string;
  /** API summary */
  summary?: string;
  /** API detailed description */
  description?: string;
  /** Query string parameter schema */
  querySchema?: z.ZodObject<any, any>;
  /** Path parameter schema */
  pathParamsSchema?: z.ZodObject<any, any>;
  /** Request body schema */
  bodySchema?: z.ZodObject<any, any>;
  /** Response body schema */
  responseSchema?: z.ZodType<any, any>;
};

// Internal storage for registered APIs
const registry: OpenAPIEndpoint[] = [];

/**
 * Register an OpenAPI endpoint
 * @param data OpenAPI endpoint data
 */
export function defineOpenAPI(data: OpenAPIEndpoint) {
  registry.push(data);
}

/**
 * Retrieve registered API data
 */
export function getOpenAPIRegistry() {
  return registry;
}

2.4 Define OpenAPI Information in Server APIs

Define an API and register OpenAPI information in server/api/student.post.ts.

// server/api/student.post.ts
import { defineEventHandler, readValidatedBody } from 'h3';
import z from 'zod';

const bodySchema = z.object({
  name: z.string().min(1).max(100).describe('Student name'),
  address: z.string().min(10).max(100).describe('Student address'),
});

const responseSchema = z.object({
  message: z.string(),
});

defineOpenAPI({
  tags: ['Student'],
  path: '/api/student',
  method: 'post',
  operationId: 'student-add',
  summary: 'Add a student',
  description: 'Add student information',
  bodySchema,
  responseSchema,
});

export default defineEventHandler(async (event) => {
  const params = await readValidatedBody(event, bodySchema.parse);
  console.log(params);
  return { message: 'success' };
});

2.5 Generate OpenAPI JSON Document

Create server/api/openapi.json.dev.ts to dynamically generate OpenAPI documentation.

// openapi.json.dev.ts
import { defineEventHandler } from 'h3';
import { ZodOptional } from 'zod';
import { createSchema } from 'zod-openapi';

export default defineEventHandler(() => {
  const endpoints = getOpenAPIRegistry();
  const paths: Record<string, any> = {};

  endpoints.forEach((ep) => {
    if (!paths[ep.path]) {
      paths[ep.path] = {};
    }
    const method = ep.method.toLowerCase();

    // Query String Parameters
    const queryParameters = ep.querySchema
      ? Object.entries(ep.querySchema.shape).map(([name, schema]) => {
          return {
            name,
            in: 'query',
            required: !(schema instanceof ZodOptional),
            ...createSchema(schema as any),
          };
        })
      : [];

    // Path Parameters
    const pathParameters = ep.pathParamsSchema
      ? Object.entries(ep.pathParamsSchema.shape).map(([name, schema]) => {
          return {
            name,
            in: 'path',
            required: true,
            ...createSchema(schema as any),
          };
        })
      : [];

    const parameters = [...pathParameters, ...queryParameters];

    // Request Body
    const requestBody = ep.bodySchema
      ? {
          content: {
            'application/json': createSchema(ep.bodySchema),
          },
        }
      : undefined;

    // Responses
    const responses = ep.responseSchema
      ? {
          200: {
            description: 'Successful response',
            content: {
              'application/json': createSchema(ep.responseSchema),
            },
          },
        }
      : {
          200: { description: 'Successful response' },
        };

    paths[ep.path][method] = {
      tags: ep.tags,
      operationId: ep.operationId || `${method}_${ep.path}`,
      summary: ep.summary,
      description: ep.description,
      parameters,
      ...(requestBody ? { requestBody } : {}),
      responses,
    };
  });

  return {
    openapi: '3.0.0',
    info: {
      title: 'My API test',
      version: '1.0.0',
      description: 'This is a test API',
    },
    servers: [{ url: 'http://localhost:3000' }],
    tags: [{ name: 'Student', description: 'Student-related operations' }],
    paths,
  };
});

3. Summary

This approach, using zod-openapi combined with the defineOpenAPI mechanism, enables automated API documentation generation while offering Zod integration and API filtering, making OpenAPI generation more flexible and efficient.


Previous article: Nuxt3 Best Practices 02: Request Validation

About

A personal blog sharing technical insights, experiences and thoughts

Quick Links

Contact

  • Email: hushukang_blog@proton.me
  • GitHub

© 2025 Swift Code Chronicles. All rights reserved