Automatic build examples for Swagger NestJs

Maybe someone will need this hack, because I couldn't find a solution that suits my problem in the official Swagger documentation.

The essence of the problem

The minimum description of Swagger, namely the example field, the testing team needed to see all the request fields and its types.

The description of fields was too complicated to do, some queries were collected from mapped data of different entities, plus it heavily loaded the code base, Entity became difficult to read due to the load of decorators of field description, validation, and now the description of swapper types.

For me, the very fact that I can’t throw an entity already described in ts or even just a class or interface with types and fields into Swagger was strange.

Our application has undergone many tests and the decision was made to take as a basis the data that we receive from the front and accept it as the truth of what data we should receive. And I wrote such a method myself to close the business need for current fields.

I am not calling on anyone to do exactly the same, but this can help those who are absolutely sure that this can solve their problem just like ours.

Implementation and data collection

if(process.env.DEV) {  
  app.use(GatherRequests);
}

First, let's connect our custom middleware GatherRequests . Its task is to collect data from Request, change it to the format we need, and then write this data to a file. apiData.json .

export const GatherRequests = (req,res,next) => {

  // сгружаем имеющиеся данные
  const apiData: any = loadApiData(apiDataPath);

  // преобразуем в объект
  const transformedObject = transformObject(JSON.parse(JSON.stringify(req.body)));

  // это метод который заменяет id url с фронта на *
  // например http://localhost:3000/page/18cfb1b0-7e01-423c-a44f-d84a30c39bd1/search
  // на http://localhost:3000/page/*/search
  const newUrl = replaceUUIDWithId(req.url);

  // Приводим данные с request в нужный нам формат
  const reqData = {
    url: newUrl,
    method: req.method.toLowerCase(),
    body: transformedObject
  }

  // проверяем есть ли идентичная дата в файле apiData.json, если нет то обновляем
  // В файле apiData.json хранится объект Map, ключем которого служит url
  const isNeededToUpdate = () => {

    const challengerData:any = reqData
    const apiDataItem:any =  apiData.get(reqData.url)



    if(!(challengerData?.body)) {
      return false
    }

    if(!apiData.has(reqData.url)) {
      return true
    }

    const challengerDataKeys = Object.keys(challengerData.body)
    const apiDataItemKeys:any =  Object.keys(apiDataItem.body)


    return apiDataItemKeys.length < challengerDataKeys.length;
  }

  // Пропускаем если обновление не требуется
  if (!isNeededToUpdate()) {
    return next()
  }

  // Обновляем если данные неактуальные
  apiData.set(reqData.url, reqData);
  saveApiData(apiData, apiDataPath)

  next();
}

Working with the json file itself occurs through two functions: saveApiData And loadApiData

const saveApiData = (data, filePath) => {
  fs.writeFileSync(filePath, JSON.stringify([...data]), 'utf-8');
}

export const loadApiData = (filePath: string) => {
  if (fs.existsSync(filePath)) {
    const fileData = fs.readFileSync(filePath, 'utf-8');
    return new Map(JSON.parse(fileData));
  }
  return new Map();
}

Let's take a closer look at the function transformObject

const transformObject = (input) => {
  const transformed = {};

  for (const key in input) {
    if (input.hasOwnProperty(key)) {
      const value = input[key];
      const uuidRegex = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;

      if (typeof value === 'string') {
        if(value.match(uuidRegex)) {
          transformed[key] = 'id';
        } else {
          transformed[key] = 'string';
        }
      } else if (typeof value === 'number') {
        transformed[key] = 1000;
      } else if (typeof value === 'symbol') {
        transformed[key] = 'symbol';
      } else if (Array.isArray(value)) {
        transformed[key] = [];
      } else if (typeof value === 'object' && value !== null) {
        transformed[key] = {};
      } else {
        transformed[key] = value;
      }
    }
  }

  return transformed;
}

There is nothing unusual in this case, I don’t want to show it off Swagger example real data, it is enough to understand the field type, so I calculate it and replace it with a string, if I encounter a uuid, I change it to a string id, so that testing would understand that the field clearly refers to the id.

Example:

{
  "name": "string",
  "projectId": "id",
  "customInformation": {}
}

As can be seen from the first block of the method implementation code GatherRequests, it only works in Dev mode.

Now we store all data about requests of the entire api in a file apiData.json

Let's now go back to the place where we connect directly Swagger

Working with Swagger documents

 const docOptions = new DocumentBuilder()
      .setTitle('Habr')
      .setDescription('The Habr API description')
      .setVersion('1.0')
      .addTag('Habr')
      .build();

 const document = SwaggerModule.createDocument(app, docOptions);

This is a basic connection. Swagger V NestJs. But before you configure it, you need to configure the module and specify it url we are moderating the object document directly, making some of my own changes there

  const apiDataPath = path.join(Paths.src, 'apiData.json');
  const apiData = await loadApiData(apiDataPath);

// Проходим по документу Swagger и модифицируем его поле example 
 Object.entries(document.paths).forEach(([url]) => {

      // В GatherRequests мы подменяли id на * в url, а здесь мы приводим все в формат Swagger
      // /v2/api/product/* --> /v2/api/product/{id}
      const apiDataUrl = replaceBracesWithAsterisk(url)

      // Если находим соответствующи урлы то модифицируем объект document
      if(apiData.has(apiDataUrl)) {
        const apiDataObj:any = apiData.get(apiDataUrl)
        document.paths[url][apiDataObj.method].responses[200] = {
          content: {
            'application/json': {
              example: apiDataObj.body
            }
          }
        }
      }
    });

  // _____________Настраиваем Swagger__________________

  SwaggerModule.setup(`/api/docs`, app, document);

Conclusion

This solution is not a magic wand, but only a narrowly focused task. If you have the ability to describe entities in a standard way in Swagger, use it.


I hope you liked my article, if it was interesting for you or helped you in any way, give it a like, I am glad to see that I am sharing my experience for a reason.

My linkedIn

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *