fastify / fastify-swagger

Swagger documentation generator for Fastify
MIT License
910 stars 200 forks source link

Trouble generating separate Swagger documentation for different versions of routes using Fastify Swagger and Fastify Swagger UI #723

Closed Towerss closed 1 year ago

Towerss commented 1 year ago

Prerequisites

Fastify version

4.17.0

Plugin version

8.4.0

Node.js version

18.16.0

Operating system

Windows

Operating system version (i.e. 20.04, 11.3, 10)

10

Description

Hello, Fastify community,

I'm currently working on a Fastify project where I aim to generate separate Swagger documentation for different versions of my API routes (e.g., v1, v2, etc.). I'm using @fastify/swagger@8.4.0, @fastify/swagger-ui@1.8.1, @fastify/autoload@5.7.1, fastify@4.17.0, and fastify-plugin@4.5.0.

I have the following folder structure:

├── src | ├── app.ts | ├── config | ├── controllers | ├── middleware | ├── models | ├── plugins | | ├── cors.ts | | ├── helmet.ts | | ├── sensible.ts | | └── swagger.ts | ├── routes | | ├── v1 | | └── v2 | ├── server.ts | ├── services | └── utils | └── tsconfig.json └── tsconfig.json

I'm running into issues where the paths object in the transformSpecification function seems to be empty. There are no paths for the @fastify/swagger-ui plugin to render:

{
 "swagger": "2.0",
 "info": {
  "version": "8.4.0",
  "title": "@fastify/swagger"
 },
 "definitions": {},
 "paths": {}
}

So the UI renders without any data:

image

I wondered if anyone had any insights into what I might be doing wrong or if there are alternative approaches to achieve the same goal. Any help or guidance would be greatly appreciated!

Steps to Reproduce

Use @fastify/autoload@5.7.1 to load plugins and routes:

    void fastify.register(AutoLoad, {
        dir: join(__dirname, 'plugins'),
        options: opts,
    });

    void fastify.register(AutoLoad, {
        dir: join(__dirname, 'routes'),
        options: opts,
    });

Register routes as plugins. this route is root in folder v1:

const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
    fastify.get(
        '/',
        {
            schema: {
                response: {
                    200: {
                        description: 'Successful response',
                        type: 'object',
                        properties: {
                            version: { type: 'number' },
                        },
                    },
                    default: {
                        description: 'Default response',
                        type: 'object',
                        properties: {
                            version: { type: 'number' },
                        },
                    },
                },
            },
        },
        async function (request, reply) {
            return { version: 1 };
        }
    );
};

export default root;

And this is the swagger plugin registration:

import { FastifyInstance } from 'fastify';
import fp from 'fastify-plugin';
import fastifySwagger from '@fastify/swagger';
import fastifySwaggerUi from '@fastify/swagger-ui';

const supportedVersions = ['v1', 'v2'];

export default fp(async (fastify: FastifyInstance) => {
    supportedVersions.forEach((version) => {
        fastify.register(
            async function (fastify, opts) {
                fastify.register(fastifySwagger);

                fastify.register(fastifySwaggerUi, {
                    routePrefix: `/documentation`,
                    uiConfig: {
                        docExpansion: 'full',
                        deepLinking: false,
                    },
                    transformSpecification: (swaggerObject, request, reply) => {
                        let tempSwaggerObject: Record<string, any> = swaggerObject;
                        let version = request.url.split('/').find((item) => item.startsWith('v'));

                        tempSwaggerObject.info = {
                            title: 'Test',
                            description: `Test API documentation for version ${version}`,
                            version: `${version}`,
                        };

                        let tempPaths = tempSwaggerObject.paths;

                        Object.keys(tempPaths).forEach((path) => {
                            if (!path.startsWith(`/${version}`)) {
                                delete tempPaths[path];
                            }
                        });

                        tempSwaggerObject.paths = { ...tempPaths };
                        return tempSwaggerObject;
                    },
                    transformSpecificationClone: true,
                });
            },
            { prefix: `/${version}` }
        );
    });
});

Expected Behavior

That documentation is generated for different root routes, in this case, for different versions of an API, so that visiting:

   http://127.0.0.1:4000/v1/documentation/static/index.html
   http://127.0.0.1:4000/v2/documentation/static/index.html

It would show only the documentation for the respective v1 or v2 routes and sub-routes.

mcollina commented 1 year ago

In order to have the setup you want, move the swagger definition in both routes/v1/ and routes/v2.

Even better, adopt a modular monolith approach https://github.com/mcollina/modular_monolith/tree/main/modular. See also https://www.youtube.com/watch?v=e1jkA-ee_aY.

Towerss commented 1 year ago

Thank you, @mcollina, for taking the time to guide me. I watched the presentation and followed the Git repo you shared. I tried to implement a modular monolith, so I modified the app.ts file autoload:

    fastify.register(AutoLoad, {
        dir: join(__dirname, 'modules'),
        encapsulate: false,
        maxDepth: 1,
    });

Created the modules folder, and inside the modules folder I moved my routes in, so it looks like this:

|  ├── modules
|   |  ├── v1
|   |   |  ├── index.ts
|   |   |  ├── plugins
|   |   |   |  └── swagger.ts
|   |   |  └── routes
|   |   |     ├── auth
|   |   |     └── root.ts
|   |  └── v2
|   |     ├── index.ts
|   |     ├── plugins
|   |     └── routes
|   |        └── root.ts

This is the consent of the index.ts files I have under both, v1 and v2 routes:

import { FastifyInstance } from 'fastify';
import fp from 'fastify-plugin';
import autoload from '@fastify/autoload';
import { join } from 'path';

export default fp(async (fastify: FastifyInstance, opts) => {
    fastify.register(autoload, {
        dir: join(__dirname, 'plugins'),
        options: {
            prefix: opts.prefix,
        },
    });

    fastify.register(autoload, {
        dir: join(__dirname, 'routes'),
        options: {
            prefix: opts.prefix,
        },
    });

    console.log(`Prefix: ${opts.prefix}`);
});

And that works well rendering the swagger documentation under route v1, and I can see the docs added when I use fastify.printRoutes():

└── (empty root node)
    ├── /
    │   └── v
    │       ├── 1 (GET, HEAD)
    │       │   └── / (GET, HEAD)
    │       │       ├── documentation (GET, HEAD)
    │       │       │   └── / (GET, HEAD)
    │       │       │       ├── static/
    │       │       │       │   ├── index.html (GET, HEAD)
    │       │       │       │   ├── swagger-initializer.js (GET, HEAD)
    │       │       │       │   └── * (HEAD, GET)
    │       │       │       ├── json (GET, HEAD)
    │       │       │       ├── yaml (GET, HEAD)
    │       │       │       └── * (GET, HEAD)
    │       │       └── auth/
    │       └── 2 (GET, HEAD)
    │           └── / (GET, HEAD)
    └── * (OPTIONS)

However, when I add a second swagger file at v2/plugins/swagger.ts, I receive this error, and the server does not start:

[ERROR] 14:00:27 FastifyError: The decorator 'swagger' has already been added!

I think there should be a way to make the routePrefix option for @fastify/swagger-ui dynamic, or maybe somehow make it accessible in the swaggerObject, so it can be modified with the transformSpecification: (swaggerObject, request, reply) => {} function.

If you could, please give me more hints, that would be really appreciated.

Thank you for your time.

mcollina commented 1 year ago

For this to work, you need to remove the use of fastify-plugin in your modules/v1/index.ts and modules/v2/index.ts.

Towerss commented 1 year ago

Thank you, @mcollina for your help.