MWAN MOBILE

×
mwan_logo
blog-banner

Swagger API Documentation Tips and Tricks in NestJS

Application Development 08-Feb-2023

The main goal of any code documentation is to transfer information from the creator to the users on how to use something. Great documentation is concise. At the same time, it has to provide enough details for users to understand everything.

As server-side developers, we need to explain how to use the APIs we’ve created to our client-side buddies! Naturally we wouldn’t want them diving into the backend codebase looking for clues, not to mention how time consuming that would be!

Read on to learn how we can improve our documentation workflow, in this article let’s focus on a classic use case – creating docs for a REST API.

The most used specification for this kind of task, by far, is OpenAPI (maintained by the OpenAPI initiative). If you’re using NestJS, there is a wonderful @nestjs/swagger module in the NestJS ecosystem that will help us a lot here! Allowing us to generate documentation simply based on our code (utilizing the NestJS CLI Plugin).

Generating documentation based on our code is always an ideal approach (if possible). Code is the main source of truth. If our documentation is generated based on it, we don’t need to worry about keeping documentation in sync with our codebases changes over time.

The @NestJS/Swagger module documentation is very descriptive, and I suggest going through it first, if you haven’t already!

So let’s get started, and dive into some tips/tricks we use in most of our projects at Trilon.

Markdown in OpenAPI documentation description

We use this feature to give an overview about the documentation itself:

  • what user roles do we have
  • what auth types are available
  • custom error codes
  • custom rules
  • etc…

To add it, just pass the markdown string into the setDescription method of DocumentBuilder:

const swaggerSettings = new DocumentBuilder()
  .setTitle('Cats API')
  .setDescription(`### your descriptive markdown API overview goes here`)
  .build();

Hint: Use code to get a list of all the available roles, auth types, etc. That way you won’t run into a situation where your documentation description is out of sync with the current state!

Markdown in OpenAPI endpoint description

We typically use this feature to specify RBAC or other endpoint requirements. If there is something else complex going on, we usually describe it here as well.

Annotate controller method with @ApiOperation and pass a markdown string in the description:

@ApiOperation({ description: '##your MD description goes here' })

Hint: Combine with RBAC decorators to avoid code duplication

Markdown in OpenAPI endpoint error description

I am usually using this feature to organize information about an error. It’s really handy when we have custom error codes and we want to give a table of exceptions that are returned by the endpoint.

To use this functionality, annotate your controller method with the @ApiResponse decorator and pass in the markdown string in the description field like so:

@ApiResponse({
  description: `### your descriptive error status overview goes here`,
  status: HttpStatus.BAD_REQUEST,
  type: EntityExceptionDto,
})

Tip: Create a custom decorator that will generate a markdown string based on a list of exceptions

An example of such a custom decorator that accepts an array of exceptions could be something like this:

class BaseDomainException extends Error {
  code: number;
  message: string;
}

type errorStuff = { name: string; code: number; message: string };

function generateExceptionsTableMarkdown(errors: errorStuff[]) {
  const descriptionBase = 'exception name | code | message \n |---|---|---|';
  const sortedErrors = errors.sort((a, b) => a.code - b.code);
  return (
    descriptionBase +
    sortedErrors.map((i) => `\n ${i.name} | ${i.code} | ${i.message}`).join('')
  );
}

export const ApiExceptions = (baseExceptions: typeof BaseDomainException[]) => {
  const errorCodeObjects = baseExceptions.map((cls) => {
  const { code, message } = new cls();
  return { name: cls.name, code, message };
});

  const description = generateExceptionsTableMarkdown(errorCodeObjects);
  const data = {
    description,
    status: HttpStatus.BAD_REQUEST,
    type: EntityExceptionDto,
  };
  return applyDecorators(ApiResponse(data));
};

Secure OpenAPI documentation (username/password)

This feature is handy when you want to secure access to your API documentation.

To add this security functionality, use the following snippet, and add the code into your application bootstrap function:

const apiDocumentationCredentials = {
  name: 'admin',
  pass: 'admin'
}

async function bootstrap() {
  const app = await NestFactory.create<INestApplication>(ApplicationModule);
  const httpAdapter = app.getHttpAdapter();

  httpAdapter.use('/api-docs', (req, res, next) => {
    function parseAuthHeader(input: string): { name: string; pass: string } {
      const [, encodedPart] = input.split(' ');

      const buff = Buffer.from(encodedPart, 'base64');
      const text = buff.toString('ascii');
      const [name, pass] = text.split(':');

      return {name, pass};
    }

    function unauthorizedResponse(): void {
      if (httpAdapter.getType() === 'fastify') {
        res.statusCode = 401;
        res.setHeader('WWW-Authenticate', 'Basic');
      } else {
        res.status(401);
        res.set('WWW-Authenticate', 'Basic');
      }

      next();
    }

    if (!req.headers.authorization) {
      return unauthorizedResponse();
    }

    const credentials = parseAuthHeader(req.headers.authorization);

    if (
      credentials?.name !== apiDocumentationCredentials.name ||
      credentials?.pass !== apiDocumentationCredentials.pass
    ) {
      return unauthorizedResponse();
    }

    next();
  });
}

Pre-authentication

This feature is handy when you want to reduce the number of steps needed to check out a protected endpoint. It is especially convenient when accessToken TTL (time to live) is very short, as you can add logic to refresh the accessToken periodically and seamlessly using protected endpoints.

To enable this, let’s first start by creating a new served folder in our root directory.

Now let’s add a useStaticAssets line in our application bootstrap function, pointing to the path of our new folder, like so:

app.useStaticAssets('served');

Next, inside of our served folder, let’s create a swagger – swagger-custom.js.

Inside the file we can implement our pre-authentication. An example of which could look something like this:

// swagger-custom.js file

async function postData(url, data = {}) {
  const response = await fetch(url, {
    method: 'POST',
    mode: 'cors',
    cache: 'no-cache',
    credentials: 'same-origin',
    headers: {'Content-Type': 'application/json'},
    redirect: 'follow',
    referrerPolicy: 'no-referrer',
    body: JSON.stringify(data),
  });

  if (response.status >= 400) {
    throw new Error('invalid credentials');
  }
  return response.json();
}

const AUTH_CREDENTIALS = {
    username: 'user123',
    password: 'qwerty@123'
}

postData('/api/auth/sign-in', AUTH_CREDENTIALS)
  .then((data) => {
    setTimeout(() => {
      window.ui.preauthorizeApiKey('accessToken', data.accessToken);
      console.log('preauth success');
    }, 1000);
  })
  .catch((e) => {
    console.error(`preauth failed: ${e}`);
  });

Next, we would need to register this file by passing it into the customJs property in our SwaggerModule options (in our main.ts / bootstrap file)

SwaggerModule.setup(config.swaggerRelPath, app, document, {
  customJs: '/swagger-custom.js', // 👈👈
  customCssUrl: '/swagger-theme.css',
});

Preauth alternatives

A simple alternative to pre-auth is persistAuthorization.

If set to true, it persists authorization data, and it would not be lost on browser close/refresh. Enable it by setting swaggerOptions.persistAuthorization = true in SwaggerModule options:

SwaggerModule.setup(config.swaggerRelPath, app, document, {
  swaggerOptions: {
    persistAuthorization: true, // 👈👈
  },
});

Custom styles, icons

In case we want to give our documentation some uniqueness & branding, we can apply custom styles and change any icons we wish. To achieve this, we can register these (ico / css) files by passing customfavIcon & customCssUrl into our SwaggerModule options:

SwaggerModule.setup(config.swaggerRelPath, app, document, {
  customfavIcon: '/custom.ico', // 👈👈
  customCssUrl: '/custom.css', // 👈👈
});

These have been some of our most used tips & tricks, but you can always come up with improvements and there are so many other options to chose from!

Start exploring the swagger ui configuration and see if you can find anything else that can help solve some of your API documentation needs.

Source: Trilon