Swagger API Documentation Tips and Tricks in NestJS

Ilya Moroz | Trilon Consulting
Ilya Moroz

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.


Learn NestJS - Official NestJS Courses 📚

Level-up your NestJS and Node.js ecosystem skills in these incremental workshop-style courses, from the NestJS Creator himself, and help support the NestJS framework! 🐈

🚀 The NestJS Fundamentals Course is now LIVE and 25% off for a limited time!

🎉 NEW - NestJS Course Extensions now live!
#NestJS
#NodeJS
#Swagger
#Documentation

Share this Post!

📬 Trilon Newsletter

Stay up to date with all the latest Articles & News!

More from the Trilon Blog .

Daniel De Lucca | Trilon Consulting
Daniel De Lucca

Fundamentals of Automated Testing - Unit Tests Basic Concepts

This is the first article in a series to understand the fundamentals of automated testing with NestJS. In this part we're going to understand the basic concepts and components of unit testing in general.

Read More
Jay McDoniel | Trilon Consulting
Jay McDoniel

NestJS Authentication without Passport

Create a local login system with username and password, and persist the login using JWTs, all without the use of PassportJS

Read More
Thiago Martins | Trilon Consulting
Thiago Martins

Advanced Testing Strategies with Mocks

Creating unit tests requires mocks or stubs to serve as proxy for some dependencies. In some cases, these dependencies are not so easily replaceable because they have many methods and/or objects as parameters. Let's learn some strategies to easily mock them.

Read More

What we do at Trilon .

At Trilon, our goal is to help elevate teams - giving them the push they need to truly succeed in today's ever-changing tech world.

Trilon - Consulting

Consulting .

Let us help take your Application to the next level - planning the next big steps, reviewing architecture, and brainstorming with the team to ensure you achieve your most ambitious goals!

Trilon - Development and Team Augmentation

Development .

Trilon can become part of your development process, making sure that you're building enterprise-grade, scalable applications with best-practices in mind, all while getting things done better and faster!

Trilon - Workshops on NestJS, Node, and other modern JavaScript topics

Workshops .

Have a Trilon team member come to YOU! Get your team up to speed with guided workshops on a huge variety of topics. Modern NodeJS (or NestJS) development, JavaScript frameworks, Reactive Programming, or anything in between! We've got you covered.

Trilon - Open-source contributors

Open-source .

We love open-source because we love giving back to the community! We help maintain & contribute to some of the largest open-source projects, and hope to always share our knowledge with the world!

Explore more

Write us a message .

Let's talk about how Trilon can help your next project get to the next level.

Rather send us an email? Write to:

hello@trilon.io
© 2019-2022 Trilon.