Зачем документировать Web API? Какой подход выбрать? С помощью каких инструментов мы можем создать документацию?

Привет, я backend-разработчик IT-компании SimbirSoft Дмитрий. В этой статье расскажу, зачем нужно документировать Web API, какие существуют подходы и покажу, как создать документацию для ASP .NET Core Web API с использованием OpenAPI/Swagger.

Материал будет полезен начинающим backend-разработчикам, которые знакомятся с Web API, а также специалистам уровня Middle, интересующимся актуальными стандартами для оформления документации.

Введение

Представим ситуацию: вы разработчик крупного онлайн-магазина с быстро растущей командой. После добавления новой функциональности в API к вам приходит аналитик или тестировщик с просьбой объяснить работу метода. На проект заходит новый разработчик и заваливает вас вопросами по работе API, а потом к нему присоединяется новичок из frontend-направления. В итоге возникает множество лишних взаимодействий с командой, которые мешают вашей продуктивной работе.

В моей практике было немало ситуаций, когда документации совсем не было, либо описание Web API велось в Postman с большим количеством устаревших запросов, либо приходилось «раскапывать» описание в многостраничной документации в Confluence. Документирование API может решить возникшую проблему и позволит команде лучше понимать работу API.

Подходы документирования Web API

Существует два подхода разработки документации:

1. Документация создается на основе кода

Здесь разработчик пишет исходный код, наполняя его комментариями (возможно вместе с аналитиком). Затем во время компиляции программы на основе исходного кода автоматически генерируется спецификация. На следующем этапе аналитик описывает пользовательскую документацию, а разработчик выпускает Web API. Такой способ считается наиболее простым, так как разработчику не требуется знать спецификацию и писать что-то помимо самого кода.

2. Документация создается на основе спецификации

Аналитик создает спецификацию Web API, на основе которой разработчик пишет исходный код. Далее по аналогии с первым подходом аналитик описывает пользовательскую документацию, и разработчик выпускает Web API. Такой подход более сложный, потому что аналитику необходимо знать язык формальных правил, с помощью которого происходит описание сущностей кода, чтобы инструмент понял написанное и сгенерировал документ. Спецификация пишется с помощью форматов JSON или YAML либо в специальном редакторе Swagger Editor.

Оба подхода позволяют генерировать код клиента на основе спецификации. Для этого можно воспользоваться инструментом Swagger Codegen.

Существует мнение, что документация на основе кода не может дать достаточного описания Web API. Я же считаю, что современных инструментов по генерации документации достаточно для полного и качественного описания Web API. Далее в статье мы рассмотрим подход создания документации на основе кода, как при этом оформить запрос и влиять на итог генерации спецификации.

Терминология и инструменты

Для начала определимся с терминами и инструментами, с помощью которых будем описывать Web API.

OpenAPI — спецификация с набором правил и стандартов, которые описывают, как должно выглядеть и работать Web API.

Swagger — это набор инструментов для работы с OpenAPI.

Swashbuckle — библиотека NuGet, которая позволяет в Web API настроить автогенерацию информации об конечных точках в соответствии со спецификацией OpenAPI. Она состоит из компонентов:

  • Swashbuckle.AspNetCore.Swagger — объектная модель Swagger и ПО промежуточного слоя для предоставления объектов SwaggerDocument в виде конечных точек JSON.

  • Swashbuckle.AspNetCore.SwaggerGen — генератор Swagger, создающий объекты SwaggerDocument непосредственно из ваших маршрутов, контроллеров и моделей. Как правило, он комбинируется с ПО промежуточного слоя в конечной точке Swagger и автоматически предоставляет Swagger JSON.

  • Swashbuckle.AspNetCore.SwaggerUI — встроенная версия средства пользовательского интерфейса Swagger. Средство интерпретирует Swagger JSON и предоставляет удобную настраиваемую среду для описания функциональности веб-API. Включает встроенные окружения тестов для открытых методов.

Установка и регистрация пакета Swashbuckle

Для установки Swashbuckle можно воспользоваться следующими способами:

  • В окне консоли диспетчера пакетов выполните следующую команду:

    Install-Package Swashbuckle.AspNetCore-Version 6.4.0

  • В диалоговом окне управления пакетами NuGet

  • щелкните правой кнопкой мыши проект в обозревателе решений>Управление пакетами NuGet;

  • в качестве источника пакета выберите nuget.org;

  • убедитесь, что параметр «Включить предварительные выпуски» включен;

  • в поле поиска введите Swashbuckle.AspNetCore;

  • выберите последний пакет Swashbuckle.AspNetCore на вкладке «Обзор» и нажмите кнопку «Установить».

 Зарегистрируйте Swagger в ConfigureServices и Configure методах файла Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc(options => options.EnableEndpointRouting = false);

    services.AddSwaggerGen();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseSwagger()
       .UseSwaggerUI(c =>
       {
           c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
       });

    app.UseMvc(
        routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "/{controller=Home}/{action=Index}/{id?}");
        }
    );
}

После регистрации откройте https://localhost:5001/swagger/v1/swagger.json. Здесь будет располагаться созданный документ, описывающий конечные точки Web API в формате JSON.

Этот документ используется для работы пользовательского интерфейса, доступного по ссылке: https://localhost:5001/swagger/index.html.

С помощью интерфейса мы можем протестировать любой метод контроллера. Выберите метод, добавьте необходимые параметры и отправьте запрос:

Конфигурация Swagger

С помощью Swagger мы можем задокументировать объектную модель и настроить пользовательский интерфейс на свое усмотрение.

Описание Web API

Метод AddSwaggerGen предназначен для добавления информации об API, сведений об авторе и лицензии.

services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Version = "v1",
        Title = "Shop API",
        Description = "Пример ASP .NET Core Web API",
        Contact = new OpenApiContact
        {
            Name = "Пример контакта",
            Url = new Uri("https://example.com/contact")
        },
        License = new OpenApiLicense
        {
            Name = "Пример лицензии",
            Url = new Uri("https://example.com/license")
        }
    });
});

После наших изменений пользовательский интерфейс примет вид:

XML-комментарии

Swashbuckle добавляет комментарии к методам и моделям из XML-документа. Чтобы его создать, перейдите к свойству проекта и установите соответствующий флаг, как показано ниже:

Подключите созданный XML-документ в AddSwaggerGen методе:

services.AddSwaggerGen(options =>
{
    var basePath = AppContext.BaseDirectory;

    var xmlPath = Path.Combine(basePath, "ShopAPI.xml");
    options.IncludeXmlComments(xmlPath);
});

Добавьте комментарии к вашим методам API:

/// <summary>
/// Создание продукта
/// </summary>
/// <param name="model">Продукт</param>
/// <returns></returns>
[HttpPost("Create")]
public IActionResult Create([FromBody] ProductModel model)
{
    return Ok(model);
}

Теперь мы знаем, для чего предназначен каждый метод API:

С помощью элемента <remarks> в комментарии можно указывать дополнительную информацию о методе. Содержимое элемента может включать простой текст, код JSON или XML. С помощью <remarks> добавим пример запроса к методу Create:

/// <summary>
/// Создание продукта
/// </summary>
/// <remarks>
/// Пример запроса:
///
///     POST /Todo
///     {
///        "id" : 1, 
///        "name" : "A4Tech Bloody B188",
///        "price" : 111,
///        "Type": "PeripheryAndAccessories"
///     }
///
/// </remarks>
/// <param name="model">Продукт</param>
/// <returns></returns>
[HttpPost("Create")]
public IActionResult Create([FromBody] ProductModel model)
{
    return Ok(model);
}

Пользовательский интерфейс становится более понятным:

Описание моделей запросов и ответов

Аналогично описанию методов API, мы можем оставлять комментарии и у моделей запросов и ответов API. А добавив контроллер System.ComponentModel.DataAnnotations, можем указать дополнительные ограничения для вводимых данных.

Добавим комментарии и аннотации к модели ProductModel:

/// <summary>
/// Продукт
/// </summary>
public class ProductModel
{
    /// <summary>
    /// Ид
    /// </summary>
    public long Id { get; set; }

    /// <summary>
    /// Наименование
    /// </summary>
    [Required(AllowEmptyStrings = false, ErrorMessage = "Наименование обязательно")]
    public string Name { get; set; }

    /// <summary>
    /// Цена
    /// </summary>
    [Required(AllowEmptyStrings = false, ErrorMessage = "Цена обязательна")]
    [Range(0.01, 1000000, ErrorMessage = "Допустимая цена от 0.01 до 1 000 000")]
    public decimal Price { get; set; }

    /// <summary>
    /// Тип продукта
    /// </summary>
    [Required(AllowEmptyStrings = false, ErrorMessage = "Тип обязателен")]
    public ProductType? Type { get; set; }

    public ProductModel()
    {
        Id = 1;
        Name = "A4Tech Bloody B188";
        Price = 111;
        Type = ProductType.PeripheryAndAccessories;
    }
}

В пользовательском интерфейсе получим следующее представление:

Примечание: для моделей запросов и ответов элемент <remarks> не отображается.

Описание типов ответов

Для работы с методами API нужно понимать, какие типы ответов и коды ошибок они возвращают. С помощью элемента <response> в комментарии мы можем задать описание для кода ответа, а воспользовавшись атрибутом [ProducesResponseType], установить тип:

/// <summary>
/// Создание продукта
/// </summary>
/// <remarks>
/// Пример запроса:
///
///     POST /Todo
///     {
///        "id" : 1, 
///        "name" : "A4Tech Bloody B188",
///        "price" : 111,
///        "Type": "PeripheryAndAccessories"
///     }
///
/// </remarks>
/// <param name="model">Продукт</param>
/// <returns></returns>
/// <response code="200">Успешное выполнение</response>
/// <response code="400">Ошибка API</response>
[HttpPost("Create")]
[ProducesResponseType(typeof(ProductModel), (int)HttpStatusCode.OK)]
[ProducesResponseType(typeof(ErrorContract), (int)HttpStatusCode.BadRequest)]
public IActionResult Create([FromBody] ProductModel model)
{
    if (!ModelState.IsValid)
    {
        var errorContract = new ErrorContract();
        errorContract.AddErrors(ModelState);
        return BadRequest(errorContract);
    }

    return Ok(model);
}

После чего в интерфейсе Swagger появилась информация о кодах ответов и их типах:

Настроив трассировку исключений нашего API, можно полностью стандартизировать возвращаемые ответы.

Перечисления

Из предыдущих примеров вы могли заметить, что перечисления отображаются как массив целых чисел и не имеют описания. Когда пользователь столкнется с тем, что в поле Type стоит 0, ему останется только предполагать о функциональном назначении этого поля.

Давайте улучшим описание наших перечислений. Создадим класс EnumTypesSchemaFilter реализующий ISchemaFilter. Этот интерфейс предназначен для работы с моделями API. Мы можем изменить модель на свое усмотрение или, как в нашем случае, добавить описание ко всем перечислениям на основе комментариев из кода:

public class EnumTypesSchemaFilter : ISchemaFilter
{
    private readonly XDocument _xmlComments;

    public EnumTypesSchemaFilter(string xmlPath)
    {
        if (File.Exists(xmlPath))
        {
            _xmlComments = XDocument.Load(xmlPath);
        }
    }

    public void Apply(OpenApiSchema schema, SchemaFilterContext context)
    {
        if (_xmlComments == null) return;

        if (schema.Enum != null && schema.Enum.Count > 0 &&
            context.Type != null && context.Type.IsEnum)
        {
            schema.Description += "<p>Содержит значения:</p><ul>";

            var fullTypeName = context.Type.FullName;

            foreach (var enumMemberName in Enum.GetValues(context.Type))
            {
                var enumMemberValue = Convert.ToInt64(enumMemberName);

                var fullEnumMemberName = $"F:{fullTypeName}.{enumMemberName}";

                var enumMemberComments = _xmlComments.Descendants("member")
                    .FirstOrDefault(m => m.Attribute("name").Value.Equals
                    (fullEnumMemberName, StringComparison.OrdinalIgnoreCase));

                if (enumMemberComments == null) continue;

                var summary = enumMemberComments.Descendants("summary").FirstOrDefault();

                if (summary == null) continue;

                schema.Description += $"<li><i>{enumMemberValue}</i> - { summary.Value.Trim()}</li>";
            }

            schema.Description += "</ul>";
        }
    }
}

И наконец подключим фильтр в AddSwaggerGen из метода ConfigureServices:

options.SchemaFilter<EnumTypesSchemaFilter>(xmlPath);

Теперь пользователи узнают содержимое каждого перечисления нашего API.

В качестве альтернативного способа добавления описания перечислениям можно воспользоваться библиотекой Unchase.Swashbuckle.AspNetCore.Extensions.

Параметры запросов

Часто заголовки запросов к нашему API содержат дополнительные параметры.

В Swashbuckle мы можем воспользоваться интерфейсом IOperationFilter для обработки запросов. Вставим поле OfficeId в каждый заголовок запросов к нашему API. Для этого создам класс SourceHeaders, реализующий IOperationFilter.

public class SourceHeaders : IOperationFilter
{
    public const string OfficeId = "officeId";

    public OpenApiSchema GuidScheme = new OpenApiSchema
    {
        Type = "string",
        Format = "uuid"
    };

    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        if (operation.Parameters == null)
            operation.Parameters = new List<OpenApiParameter>();

        if (!context.ApiDescription.ActionDescriptor.RouteValues.TryGetValue("controller",
                out string controllerName))
        {
            return;
        }

        //Добавление параметра в заголовок запроса
        operation.Parameters.Add(new OpenApiParameter
        {
            Name = OfficeId,
            In = ParameterLocation.Header,
            Schema = GuidScheme,
            Description = "Офис",
            Required = true
        });
    }
}

В интерфейсе Swagger каждому запросу добавился обязательный параметр OfficeId:

Данный способ позволяет не только вставлять поля в заголовки, но и изменять запросы на ваше усмотрение.

Авторизация

Обычно для доступа к нашему API требуется авторизация. Это повышает безопасность API. Для добавления функции авторизации в пользовательский интерфейс Swagger мы можем указать определение безопасности в AddSwaggerGen методе. Эта функция будет состоять из кнопки «Авторизовать» в верхней части страницы, которая устанавливает заголовок авторизации:

options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
    Description = @"Введите JWT токен авторизации.",
    Name = "Authorization",
    In = ParameterLocation.Header,
    Type = SecuritySchemeType.ApiKey,
    BearerFormat = "JWT",
    Scheme = "Bearer"
});

Метод расширения AddSecurityRequirement добавит заголовок авторизации к каждой конечной точке при отправке запроса:

options.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
    {
      new OpenApiSecurityScheme
      {
        Reference = new OpenApiReference
        {
            Type = ReferenceType.SecurityScheme,
            Id = "Bearer"
        },
      },
      new List<string>()
    }
});

Теперь можно зайти в пользовательский интерфейс и авторизоваться:

После авторизации к каждому запросу при отправке будет добавлен заголовок Authorization, содержащий наш токен:

Пользовательский интерфейс

Готовый пользовательский интерфейс довольно функционален и удобен. Но если вы хотите оформить API в собственном стиле, то такая возможность существует. Для этого добавим в папку wwwroot проекта собственный файл стилей.

В методе Configure включим работу со статическими файлами и подключим custom-swagger-ui.css в UseSwaggerUI:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStaticFiles();

    app.UseSwagger()
       .UseSwaggerUI(c =>
       {
           c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
           c.InjectStylesheet("/swagger/ui/custom-swagger-ui.css");
       });

    app.UseMvc(
        routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "/{controller=Home}/{action=Index}/{id?}");
        }
    );
}

Теперь наши методы API перешли на темную сторону:

Подведем итоги

  • Документирование Web API полезно не только для конечных пользователей, но и для команды разработки. Благодаря этому у специалистов, подключившимся к проекту, не будет необходимости задавать коллегам вопросы, ответы на которые прописаны в документации.

  • Существует два подхода разработки документации: на основе кода и на основе спецификации.

  • Оформить качественную документацию Web API можно и на основе кода. При этом не нужно обрабатывать каждый запрос отдельно, достаточно добавить общие фильтры. К тому же комментирование кода является хорошей практикой.

 Спасибо за внимание! Надеемся, этот материал был для вас полезен.

Литература

Документация по веб-API ASP. NET Core с использованием Swagger (OpenAPI)

Swashbuckle.AspNetCore 

Авторские материалы для разработчиков мы также публикуем в наших соцсетях – ВК и Telegram.

Комментарии (2)


  1. s207883
    23.12.2022 10:24

    Тег returns тоже можно заполнять, он будет включён в аннотацию ответа.

    Про enum и расширения было интересно, спасибо.


  1. onets
    23.12.2022 12:39
    +1

    Эта либа мало того что трудновыговариваемая, так еще и глючнее чем nswag. Из коробки проблемы с версионированными апи. Долбался, долбался - заменил на nswag, все сразу взлетело.