log
Swift Code Chronicles

Fastify Best Practices #5: Generating Swagger Documentation

Published on June 24, 2025
Updated on June 26, 2025
29 min read
TypeScript

When developing server-side APIs, maintaining accurate, real-time, and easy-to-understand API documentation is key to improving team collaboration and the external developer experience. Writing documentation by hand is not only time-consuming but also prone to inconsistencies between the code and the docs. Fortunately, in the Fastify ecosystem, we can use two powerful tools, @fastify/swagger and @fastify/swagger-ui, to directly transform our code (especially Zod schemas) into professional, interactive Swagger documentation, achieving “code as documentation.”

First, we need to install the relevant dependencies:

npm install @fastify/swagger @fastify/swagger-ui

Configuring the Swagger Plugins

Next, we need to register these two plugins with our Fastify instance. It’s good practice to handle this configuration in a separate plugin file (e.g., plugins/swagger.ts).

The core idea here is to enable @fastify/swagger to understand the Zod schemas we highly recommended in the previous article.

import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
import fastify, { FastifyInstance } from 'fastify';
import { jsonSchemaTransform } from 'fastify-type-provider-zod';

export const setupSwagger = async (server: FastifyInstance) => {
  // Register the core swagger plugin
  await server.register(swagger, {
    // Key: make swagger support Zod schemas
    transform: jsonSchemaTransform,
    openapi: {
      openapi: '3.0.0', // OpenAPI Version
      info: {
        // Basic document information
        title: 'Fastify Best Practice Project API',
        description: 'This is the API documentation for a sample project',
        version: '0.1.0',
      },
      servers: [
        // API server list
        {
          url: `http://127.0.0.1:8080`,
          description: 'Development server',
        },
      ],
      tags: [
        // API category tags
        { name: 'user', description: 'User-related APIs' },
        { name: 'health', description: 'Health check APIs' },
      ],
      // Optional: Add global security definitions
      components: {
        securitySchemes: {
          bearerAuth: {
            type: 'http',
            scheme: 'bearer',
            bearerFormat: 'JWT',
          },
        },
      },
      security: [
        {
          bearerAuth: [],
        },
      ],
    },
  });

  // Register the swagger UI plugin to provide the visual interface
  await server.register(swaggerUi, {
    routePrefix: '/doc', // Access path for the swagger documentation
    uiConfig: {
      docExpansion: 'list', // 'full', 'list', or 'none'
      deepLinking: true,
    },
  });
};

The Role of jsonSchemaTransform

As you may have noticed, the key here is the transform: jsonSchemaTransform configuration. This function, imported from fastify-type-provider-zod, acts as a bridge. It automatically converts the Zod schemas you define in your routes into the JSON Schema format required by Swagger (the OpenAPI specification). Without it, Swagger would not be able to understand our Zod type definitions.

Adding Documentation Info to Routes

After configuring the plugins, we need to provide some metadata in the schema option of each API route. Swagger reads this information to generate the documentation.

Let’s look at a concrete example:

import { FastifyInstance } from 'fastify';
import type { ZodTypeProvider } from 'fastify-type-provider-zod';
import { z } from 'zod';

const routes = async (fastify: FastifyInstance) => {
  fastify.withTypeProvider<ZodTypeProvider>().get(
    '/user/:id', // API URL
    {
      schema: {
        summary: 'Get single user information',
        description: 'Queries for user details based on the user ID.',
        tags: ['user'], // API Category
        params: z.object({
          id: z.string().uuid().describe("User's unique ID (UUID format)"),
        }),
        response: {
          200: z
            .object({
              id: z.string().uuid(),
              name: z.string(),
              email: z.string().email(),
            })
            .describe('Successful response'),
          404: z
            .object({
              message: z.string(),
            })
            .describe('Response when user is not found'),
        },
        // Indicates this endpoint requires bearerAuth
        security: [{ bearerAuth: [] }],
      },
    },
    async (req, reply) => {
      // ... business logic ...
      const user = { id: req.params.id, name: 'Millet', email: 'millet@example.com' };
      return reply.status(200).send(user);
    },
  );
};

export default routes;

Tip: Zod’s .describe() method is a very useful trick. jsonSchemaTransform intelligently captures its content and displays it as the field description in the Swagger UI, making your API documentation much clearer.

Summary

With the configuration above, we not only generate professional, interactive API documentation for our project, but more importantly, this documentation is “living documentation” that is perfectly in sync with our code. Whenever your Zod schema changes, the documentation updates automatically. This completely eliminates the problem of inconsistencies between code and documentation that arises from manual writing. It greatly improves development efficiency and the maintainability of the API, making it an indispensable part of modern server-side development.


In my open-source project, I’ve encapsulated the Swagger configuration in a separate plugin (04.swagger.plugin.ts) and loaded it automatically via fastify-autoload. You are welcome to use it as a reference:

fastify-best-practice on GitHub

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