Программирование
May 24

Настраиваем автодокументирование для express-приложений

Вместо предисловия

Долгое время в express было принято использовать решения, которые довольно большую часть работы перекладывают на разработчика. Одним из самых популярных решений и по сей день является swagger-jsdoc.

Поскольку, оно является и самым проверенным, я продолжал рассказывать о нём студентам из года в год. Но в этом году что-то пошло не так... Один из студентов спросил: "а нет ли чего-то такого же удобного, как в Nest.JS?". Тут-то всё и началось.

Я прочитал более десятка статей, пересмотрел несколько роликов в поисках оптимального решения. И вот мы здесь. Я выделил 3 решения для настройки автодокументации и подготовил сравнения и примеры для них.

Старый-добрый swagger-jsdoc

Крайне простое в использовании решение, которое требует от вас только терпения и времени. По сути, вы описываете все эндпоинты через специальные jsdoc-комментарии, по формату совпадающие с OpenAPI в YAML.

Библиотека же ходит по вашему коду, собирает все такие комментарии и встраивает их в общий файлик OpenAPI.

Пример кода:

/**
 * @openapi
 * /v1/testCreate:
 *   post:
 *     produces:
 *       - application/json
 *     parameters:
 *       - name: username
 *         in: formData
 *         required: true
 *         type: string
 *       - name: password
 *         in: formData
 *         required: true
 *         type: string
 *     description: Test create
 *     responses:
 *       200:
 *         description: Returns a created object.
 */
router
  .route('/testCreate')
  .post(exampleController.post)

Плюсы у такого решения вполне очевидны, оно достаточно простое и универсальное. Подходит, даже если вы не используете typescript на бэкенде (лучше всё-таки использовать...). Минусы, думаю, вы и сами понимаете.

Плюсы и минусы

Плюсы:

  • простота
  • универсальность

Минусы:

  • много ручной работы

Полный код примера

Полный код примера доступен на github.

Пишем как на Nest: routing-controllers

Если вы начинаете с express, но планируете перейти на Nest позднее, то routing-controllers - ваш выбор. Синтаксически это почти один-в-один совпадает с Nest.

Пример кода:

@JsonController()
export class ExampleController {
    @OpenAPI({ summary: 'Test create' })
    @ResponseSchema(TestCreateResponseDto, { statusCode: 200 })
    @Post('/testCreate')
    post(
        @Body({ type: TestCreateDto }) body: TestCreateDto,
        @Res() response: Response,
    ): void {
        const uuid: string = randomUUID();

        const responseBody = {
            uuid,
            ...body,
        };

        response.status(201).send(responseBody);
    }
}

Как видите, от привычного express тут мало что осталось, но контроллеры становятся куда более структурированными за счёт аннотаций.

Типы для документации собираются на основе class-validator, как и в Nest. Что фактические вынуждает вас к валидации входящих данных.

Стоит отметить, что само по себе, решение не предоставляет возможностей для генерации документации, это делается за счёт использования библиотеки routing-controllers-openapi. У неё есть проблема, из-за которой могут быть проблемы с работой рантайма tsx. Не могу сказать, что я как-то сильно погружался, но по моим ощущениям, это связано с некорректной обработкой данных из метадаты. К счастью, это исправляется либо за счёт monkey-патча, либо за счёт использования моего форка.

У меня есть даже свой бойлерплейт на основе routing-controllers. Правда, он не до конца написан, но начало положено...

Плюсы и минусы

Плюсы:

  • код более структурирован
  • вынужденное использование валидаторов
  • синтаксически близко к Nest

Минусы:

  • проблема с рантаймом tsx
  • остаётся ли express всё ещё таким же лёгким?

Полный код примера

Полный код примера доступен на github.

Ещё немножко декораторов: tsoa

Да, снова декораторы. Но tsoa - решение куда более комплексное, чем routing-controllers. По крайней мере, на мой взгляд.

Давайте посмотрим на пример:

@Route()
@Tags('Example')
export class ExampleController extends Controller {
    @Post('/testCreate')
    @Response<TestCreateResponseDto>(201, 'Returns a created object.')
    public async post(
        @Body() body: TestCreateDto,
    ): Promise<TestCreateResponseDto> {
        const uuid: string = randomUUID();

        return {
            uuid,
            ...body,
        };
    }
}

Это решение предоставляет минимально-необходимый набор инструментов для реализации автодокументируемого кода.

Для составления схем используются интерфейсы, валидация в них осуществляется за счёт добавления специальных комментариев. Код всё также хорошо структурирован, как и в routing-controllers. Никаких проблем с рантаймами тайпскрипта тут нет.

Для меня tsoa - самое оптимальное решение. Буду во все новые проекты на Node брать именно эту библиотеку.

Плюсы и минусы

Плюсы:

  • код более структурирован
  • встроенная валидация
  • единое решение для контроллеров и автодокументирования
  • синтаксически близко к Nest

Минусы:

  • остаётся ли express всё ещё таким лёгким?

Полный код примера

Полный код примера доступен на github.

Выводы

Хотите посмотреть на сухое сравнение, тогда вам сюда. А по моим личным ощущениям, tsoa - лучший выбор.