Scalar у Minimal API: повний проєкт і Fluent OpenAPI
Scalar у Minimal API: повний проєкт і Fluent OpenAPI
.NET 9+ RecommendedMinimal API, групувати маршрути, організовувати проєкт і проектувати HTTP-контракти. Але в реальному житті цього недостатньо. Команда бекенду не може просто сказати фронтенду: «дивись у Program.cs». Потрібна жива документація, яка будується з реального коду, оновлюється разом із ним і дозволяє одразу тестувати endpoint-и. Саме тут у гру входить Scalar.У цьому матеріалі ми не будемо використовувати атрибути на кшталт [Produces], [Tags], [EndpointSummary] або [Description]. Усе, що стосується документації, ми зробимо через Fluent API: WithSummary(), Produces(), Accepts(), WithOpenApi(), MapGroup(), AddOpenApi(...) transformers і MapScalarApiReference(...).Що побудуємо
- Minimal API проєкт, який можна створити з нуля через
dotnet new web - OpenAPI-документ на маршруті
/openapi/v1.json - Scalar UI на маршруті
/scalar - Групу endpoint-ів
/api/productsз повним Fluent-описом metadata
Що розберемо
- усі основні Fluent-методи опису endpoint-ів;
- різницю між локальним, груповим і глобальним рівнем metadata;
WithOpenApi(...)для тонкого налаштування операції;- OpenAPI transformers для параметрів, схем і документа в цілому;
- конфігурацію самого Scalar через
MapScalarApiReference(...).
Результат наприкінці
- у браузері відкривається
https://localhost:{port}/scalar; - у Scalar видно summaries, descriptions, tags, operation id, request/response schemas;
- документація відображає реальні статус-коди, тіла запитів і помилок;
- все зібрано без атрибутів.
Minimal API + OpenAPI + Scalar. Якщо вам потрібна окрема теорія про сам стандарт OpenAPI, дивіться матеріал про OpenAPI як контракт.1. Що таке Scalar і чому не достатньо просто openapi.json
Коли ви вмикаєте OpenAPI в ASP.NET Core, ви отримуєте машиночитаний документ. Це чудово для генераторів клієнтів, тестів і контрактних перевірок. Але для людини сирий JSON на кшталт /openapi/v1.json незручний:
- його важко читати очима;
- ним незручно ділитися з QA або фронтендом;
- з нього не дуже зручно робити ручні пробні запити;
- він не дає якісного UX документації.
Scalar вирішує саме цю проблему. Він бере OpenAPI-документ і рендерить з нього інтерактивний reference UI.
Ключова думка: Scalar не вигадує контракт сам. Він лише красиво і зручно показує той OpenAPI-документ, який ви згенерували з коду або з контракту.
2. Який стек ми беремо для відтворюваного проєкту
У 2026 році для нового навчального прикладу найрозумніший стартовий варіант такий:
.NET 9 або новіше. Саме з .NET 9 у ASP.NET Core є вбудована підтримка AddOpenApi() і MapOpenApi().Microsoft.AspNetCore.OpenApi для генерації OpenAPI-документа з Minimal API.Scalar.AspNetCore для рендерингу документації на маршруті /scalar.Minimal API + AddOpenApi + Scalar.AspNetCore дає чистіший стек, ніж старий маршрут через SwaggerGen як основну інтеграцію. Це не означає, що Swashbuckle поганий; це означає, що для навчального прикладу з акцентом на сучасний ASP.NET Core простіше починати з вбудованого OpenAPI API.Сумісність: рекомендований і legacy-шлях
Використовуємо:
builder.Services.AddOpenApi()app.MapOpenApi()app.MapScalarApiReference()
Це і є основний шлях у цій статті.
Якщо ви сидите на .NET 8, Scalar теж можна підключити, але зазвичай через генератор на кшталт Swashbuckle.AspNetCore:
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
if (app.Environment.IsDevelopment())
{
app.MapSwagger("/openapi/{documentName}.json");
app.MapScalarApiReference();
}
Усе інше в статті про Fluent metadata майже повністю зберігає цінність, але точні інтеграційні точки OpenAPI будуть трохи іншими.
Program.cs.3. Що саме побачить читач у кінці
Ми збудуємо невеликий каталог продуктів із такими endpoint-ами:
| Метод | Маршрут | Навіщо потрібен |
|---|---|---|
GET | /api/products | список товарів з query-параметрами |
GET | /api/products/{id} | читання одного товару |
POST | /api/products | створення товару |
PUT | /api/products/{id} | оновлення товару |
DELETE | /api/products/{id} | видалення товару |
GET | /_internal/ping | технічний маршрут, який ми сховаємо з документації |
У документації ми покажемо:
summary;description;tags;operationId;requestBody;- query і route parameters;
200,201,204,404,422,500;- моделі запитів і відповідей;
- кастомізацію OpenAPI-операцій через
WithOpenApi(...); - глобальні доповнення через transformers.
4. Створюємо проєкт з нуля
Крок 1: Генерація шаблону
mkdir MinimalApiScalarDemo
cd MinimalApiScalarDemo
dotnet new web --framework net9.0
mkdir MinimalApiScalarDemo
cd MinimalApiScalarDemo
dotnet new web --framework net9.0
Крок 2: Додаємо пакети
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet add package Scalar.AspNetCore
Крок 3: Перший запуск для перевірки бази
Поки що ми не побачимо ні Scalar, ні OpenAPI, бо ще нічого не підключали. Але важливо перевірити, що сам проєкт стартує.
dotnet watch. Натискаєте Ctrl + S у редакторі, і сервер сам перезапускається. Це критично знижує тертя, коли ви налаштовуєте metadata endpoint-ів і одразу дивитесь результат у /scalar. Увесь подальший сенс статті зводиться до одного критерію: після кожної зміни ви можете відкрити /scalar і побачити, як змінився контракт.
5. Структура проєкту
Ми не будемо складати всю логіку в один Program.cs. Для теми документації це погана ідея, бо змішаються:
- маршрути;
- бізнес-логіка;
- DTO;
- OpenAPI-конфігурація;
- Scalar-конфігурація.
Побудуємо ось таку структуру:
Чому ця структура важлива
У темі документації багато хто робить помилку: кидає всі MapGet/MapPost у Program.cs, а потім не може зрозуміти, де закінчується логіка API і де починається логіка опису документації.
Тут ми розвели відповідальність:
Program.csвідповідає за композицію;ProductEndpoints.csвідповідає за маршрути і metadata;ProductContracts.csвідповідає за схеми даних;ProductStore.csвідповідає за технічне сховище даних;launchSettings.jsonвідповідає за зручний запуск одразу в Scalar.
6. Детально розбираємо Program.cs
6.1. AddOpenApi(...)
Рядок:
builder.Services.AddOpenApi(...)
реєструє генератор OpenAPI-документа. Це не UI. Це саме механізм, який:
- збирає metadata з endpoint-ів;
- виводить моделі запитів/відповідей;
- будує OpenAPI JSON;
- дозволяє підключати transformers.
6.2. Document transformer
У AddDocumentTransformer(...) ми задаємо інформацію верхнього рівня:
Title;Version;Description;Servers.
Це рівень усього документа. Тут немає сенсу описувати окремі endpoint-и. Тут живе все, що стосується API в цілому.
6.3. Schema transformer
AddSchemaTransformer(...) працює на рівні схем. У нашому прикладі він модифікує всі decimal-поля, щоб схема явно показувала format: decimal.
Це важливо з педагогічної точки зору. У великих системах команди часто мають доменні правила:
decimalдля грошей;- усі
DateTimeOffsetз UTC-семантикою; - певні nullable-поля мають спеціальну бізнес-інтерпретацію.
Schema transformer дозволяє донести ці правила до документації без атрибутів на кожному DTO.
6.4. Operation transformer
AddOperationTransformer(...) ми використовуємо для глобального правила: кожна операція документально підтримує 500.
500. Це означає, що ми хочемо, щоб документація не брехала: будь-яке реальне серверне API має сценарій неочікуваної помилки, і споживач повинен бачити це в контракті.6.5. MapOpenApi(...)
Рядок:
app.MapOpenApi("/openapi/{documentName}.json")
реєструє endpoint, який віддає OpenAPI-документ. Це важливий нюанс: OpenAPI JSON у ASP.NET Core теж є звичайним endpoint-ом, а не якоюсь магічною окремою системою.
Тобто до нього можна застосовувати звичні endpoint-конвенції. Наприклад:
app.MapOpenApi("/openapi/{documentName}.json")
.AllowAnonymous();
6.6. MapScalarApiReference(...)
Саме цей рядок дає UI:
app.MapScalarApiReference("/scalar", options => ...)
Він каже: «створи сторінку документації на маршруті /scalar і під'єднай її до OpenAPI-документа».
7. Fluent API для Scalar: всі ключові варіанти підключення
MapScalarApiReference(...) має кілька корисних overload-ів. Це важливо, бо в реальних проєктах вам не завжди підходить один і той самий маршрут або однакова конфігурація для всіх користувачів.
app.MapScalarApiReference();
Дає стандартний маршрут /scalar з конфігурацією за замовчуванням.
app.MapScalarApiReference("/docs");
Корисно, якщо у вас корпоративна угода про URL на кшталт /docs, /reference або /api-docs.
app.MapScalarApiReference(options =>
{
options.WithTitle("My API")
.ShowOperationId();
});
Це найчастіший сценарій: маршрут стандартний, але поведінку Scalar ви тонко налаштовуєте.
app.MapScalarApiReference("/docs", options =>
{
options.WithTitle("My API Docs");
});
Оптимально, коли хочеться і нестандартний URL, і контроль над виглядом.
app.MapScalarApiReference((options, httpContext) =>
{
var isAdmin = httpContext.User.IsInRole("Admin");
options.WithTitle(isAdmin ? "Admin API" : "Public API");
});
Це вже більш просунутий сценарій. Наприклад, різні ролі бачать різний title, різні документи або різні prefilled auth settings.
Найкорисніші Fluent-методи конфігурації Scalar
/openapi/{documentName}.json.operationId у UI. Дуже корисно, якщо ви свідомо задаєте WithName(...) на endpoint-ах.C# + HttpClient.app.MapScalarApiReference("/scalar", options =>
{
options.WithTitle("Catalog API")
.WithOpenApiRoutePattern("/openapi/{documentName}.json")
.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient)
.ShowOperationId()
.ExpandAllTags()
.SortTagsAlphabetically()
.SortOperationsByMethod()
.PreserveSchemaPropertyOrder();
});
8. Усі основні Fluent API-варіанти опису endpoint-ів
Тепер переходимо до найважливішого. Коли кажуть «опис endpoint-а», на практиці мають на увазі кілька різних шарів metadata. У Minimal API без атрибутів це робиться не одним методом, а цілим набором fluent-конвенцій.
Працює на одному конкретному маршруті:
WithName(...)WithSummary(...)WithDescription(...)WithTags(...)Accepts<T>(...)Produces<T>(...)Produces(...)ProducesProblem(...)ProducesValidationProblem(...)ExcludeFromDescription()WithOpenApi(...)
Працює на групі маршрутів і зменшує дублювання:
MapGroup("/api/products").WithTags("Products").WithOpenApi().RequireAuthorization().AddEndpointFilter(...)
Це найкращий рівень для загальних правил фічі.
Працює не через RouteHandlerBuilder, а через OpenAPI generator:
AddDocumentTransformer(...)AddOperationTransformer(...)AddSchemaTransformer(...)
Це рівень для правил, які мають зачепити весь документ.
8.1. WithName(...)
.WithName("GetProductById")
Це не просто «красиве ім'я». У Minimal API ім'я endpoint-а:
- використовується для link generation;
- часто стає
operationIdв OpenAPI; - покращує читабельність у Scalar, якщо ви вмикаєте
ShowOperationId().
8.2. WithSummary(...)
.WithSummary("Отримати товар за ID")
Це короткий заголовок операції. Саме summary найчастіше відображається в списку операцій у Scalar.
8.3. WithDescription(...)
.WithDescription("Повертає розширену модель одного товару.")
Description має бути довшим, ніж summary. Це місце для контексту:
- бізнес-сенс операції;
- важливі обмеження;
- пояснення edge cases;
- уточнення щодо route/query/body semantics.
8.4. WithTags(...)
.WithTags("Products")
Tags групують операції в документації. Якщо ви забудете про теги, великий API швидко перетвориться на нечитабельний список із сотні маршрутів.
8.5. Accepts<T>(...)
.Accepts<CreateProductRequest>("application/json")
Цей метод формалізує requestBody. Він потрібен не завжди, бо частину речей generator може вивести сам з сигнатури handler-а, але:
- він корисний для явності;
- він корисний при нестандартних content-type;
- він підкреслює намір прямо в fluent-конфігурації.
8.6. Produces<T>(...)
.Produces<ProductDetailsResponse>(StatusCodes.Status201Created)
Це основний спосіб явно задати тіло успішної відповіді.
8.7. Produces(...)
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound)
Коли тіло відповіді не потрібне або неважливе, достатньо вказати статус-код.
8.8. ProducesProblem(...)
.ProducesProblem(StatusCodes.Status500InternalServerError)
Цей метод показує, що у відповіді використовується ProblemDetails-стиль для помилок.
8.9. ProducesValidationProblem(...)
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity)
Дуже корисно для endpoint-ів, які приймають тіло запиту й можуть повертати помилки валідації.
8.10. ExcludeFromDescription()
app.MapGet("/_internal/ping", ...)
.ExcludeFromDescription();
Це варіант для внутрішніх, службових або технічних маршрутів, які не повинні засмічувати публічну документацію.
8.11. WithOpenApi(...)
Це найпотужніший локальний інструмент. Він дозволяє взяти вже згенеровану OpenAPI-операцію і допрацювати її вручну:
- описати параметри;
- помітити endpoint як deprecated;
- додати приклади;
- підправити summary/description;
- встановити кастомні responses, extensions тощо.
.WithOpenApi(operation =>
{
var id = operation.Parameters.First(p => p.Name == "id");
id.Description = "GUID ідентифікатор товару.";
return operation;
});
WithOpenApi(...) і transformers рятують нас у сценарії «без атрибутів». У built-in metadata API немає окремого fluent-методу типу DescribeParameter("id", "..."), тому тонкий опис параметрів ми робимо або тут, або глобально через operation transformer.9. Детально: як описувати endpoint-и без атрибутів
Тепер розберемо кожен наш маршрут як інженерний приклад.
9.1. GET /api/products
group.MapGet("/", GetProducts)
.WithName("GetProducts")
.WithSummary("Отримати список товарів")
.WithDescription("Повертає каталог продуктів з optional query-фільтрами.")
.Produces<IReadOnlyList<ProductListItemResponse>>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithOpenApi(operation =>
{
var search = operation.Parameters.First(p => p.Name == "search");
search.Description = "Пошук по фрагменту назви.";
var includeInactive = operation.Parameters.First(p => p.Name == "includeInactive");
includeInactive.Description = "Чи включати неактивні товари.";
return operation;
});
Що тут важливо:
WithName()задає стабільнийoperationId.WithSummary()іWithDescription()дають короткий та довгий опис.Produces<T>()фіксує успішний payload.WithOpenApi(...)дозволяє описати query-параметри без атрибутів[Description].
9.2. GET /api/products/{id}
group.MapGet("/{id:guid}", GetProductById)
.WithName("GetProductById")
.WithSummary("Отримати товар за ID")
.WithDescription("Повертає розширену модель одного товару.")
.Produces<ProductDetailsResponse>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithOpenApi(operation =>
{
var id = operation.Parameters.First(p => p.Name == "id");
id.Description = "GUID ідентифікатор товару.";
return operation;
});
Цей приклад демонструє route parameter + кілька статус-кодів.
9.3. POST /api/products
group.MapPost("/", CreateProduct)
.WithName("CreateProduct")
.WithSummary("Створити товар")
.WithDescription("Створює новий товар у пам'яті та повертає його деталі.")
.Accepts<CreateProductRequest>("application/json")
.Produces<ProductDetailsResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem(StatusCodes.Status422UnprocessableEntity)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithOpenApi(operation =>
{
operation.RequestBody!.Description = "JSON payload для створення товару.";
return operation;
});
Тут ми бачимо вже повний набір для mutation-операції:
Accepts<T>()для тіла запиту;Produces<T>(201)для успішного створення;ProducesValidationProblem(422)для бізнес- або input-validation;WithOpenApi(...)для опису request body.
9.4. PUT /api/products/{id}
Цей маршрут поєднує все одразу:
- route parameter;
- request body;
200;404;422.
Це хороший «еталонний» endpoint для API-документації, бо він показує складніший сценарій, ніж простий GET.
9.5. DELETE /api/products/{id}
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound)
Тут навмисно нема Produces<T>(), бо DELETE у нас повертає 204 No Content. Це важливий дидактичний момент: не всі endpoint-и мають JSON-тіло у відповіді.
10. Де саме проходить межа між Route Metadata і OpenAPI customization
Це питання дуже часто плутають.
| Рівень | Інструмент | Для чого підходить |
|---|---|---|
| Route metadata | WithSummary, Produces, Accepts, WithTags | Основний контракт endpoint-а |
| Per-operation OpenAPI patch | WithOpenApi(...) | Точкове ручне доопрацювання конкретної операції |
| Global document customization | AddDocumentTransformer(...) | Назва API, сервери, top-level поля |
| Global operation customization | AddOperationTransformer(...) | Спільні відповіді, security, параметри, умовні правила |
| Global schema customization | AddSchemaTransformer(...) | Опис або формат типів у моделях |
WithOpenApi(...) як місце, де ви описуєте все підряд. Якщо у вас 30 endpoint-ів і кожен отримує вручну по 20 мутацій OpenAPI-об'єкта, документація стає не менш хаотичною, ніж атрибути, від яких ви намагалися піти.Правильне правило таке:
- 80% metadata задаємо простими fluent-методами на endpoint-і;
- 15% вирішуємо груповими конвенціями через
MapGroup; - 5% добиваємо
WithOpenApi(...)або transformers.
11. Fluent API на рівні Route Group
У нашому коді є ось цей рядок:
var group = app.MapGroup("/api/products")
.WithTags("Products")
.WithOpenApi();
Чому це добре:
- префікс
/api/productsзадано один раз; - теги задаються для всієї групи;
- група одразу «видима» для OpenAPI як логічний блок;
- далі конкретні endpoint-и дописують лише специфічні деталі.
Що ще часто ставлять на групу
MapGroup майже завжди є кращим стартом, ніж розрізнені app.MapGet(...) по всьому Program.cs. Це не лише питання чистоти коду, а й питання якості документації в Scalar.12. Parameter descriptions без атрибутів: справжня складність
Саме тут більшість розробників і спотикається. Для summaries, descriptions, responses, tags і request bodies у нас є зручні fluent-методи. Але для опису окремих route/query/header parameter-ів окремого простого fluent-методу в Minimal API немає.
Тому в сценарії без атрибутів залишаються два реальні інструменти:
WithOpenApi(...)на конкретному endpoint-і;AddOperationTransformer(...)глобально.
Локальний варіант через WithOpenApi(...)
Ми вже бачили його вище:
.WithOpenApi(operation =>
{
var search = operation.Parameters.First(p => p.Name == "search");
search.Description = "Пошук по фрагменту назви.";
return operation;
});
Це найкраще, коли:
- endpoint-ів небагато;
- параметр специфічний лише для однієї операції;
- ви хочете тримати опис прямо біля route definition.
Глобальний варіант через AddOperationTransformer(...)
Приклад:
builder.Services.AddOpenApi(options =>
{
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
if (context.Description.RelativePath == "api/products/{id}" &&
context.Description.HttpMethod == "GET")
{
var id = operation.Parameters.FirstOrDefault(p => p.Name == "id");
if (id is not null)
{
id.Description = "GUID ідентифікатор товару.";
}
}
return Task.CompletedTask;
});
});
Такий варіант кращий, коли:
- ви хочете централізувати правила;
- однакові параметри повторюються в багатьох endpoint-ах;
- документація має спільні корпоративні норми.
13. WithOpenApi(...): усі реальні сценарії, які варто знати
Нижче не просто теорія, а список сценаріїв, де WithOpenApi(...) реально виправданий.
.WithOpenApi(operation =>
{
var id = operation.Parameters.First(p => p.Name == "id");
id.Description = "GUID ідентифікатор товару.";
return operation;
})
.WithOpenApi(operation =>
{
operation.Deprecated = true;
return operation;
})
.WithOpenApi(operation =>
{
operation.Summary = "Кастомний summary";
operation.Description = "Кастомний description";
return operation;
})
.WithOpenApi(operation =>
{
operation.RequestBody!.Description = "JSON payload для створення товару.";
return operation;
})
.WithOpenApi(operation =>
{
operation.Responses["201"].Description = "Resource created successfully.";
return operation;
})
Коли WithOpenApi(...) не потрібен
Не треба лізти в нього, якщо те саме можна виразити простіше через:
WithSummary(...);WithDescription(...);WithTags(...);Accepts<T>(...);Produces<T>(...);ProducesProblem(...);ProducesValidationProblem(...).
Принцип дуже простий: спочатку використовуйте найбільш декларативний fluent-метод, а лише потім переходьте до ручної мутації OpenAPI-операції.
14. Повний Program.cs ще раз, але як історія рішення
using Microsoft.OpenApi.Models;
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<ProductStore>();
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, context, cancellationToken) =>
{
document.Info.Title = "Minimal API + Scalar Demo";
document.Info.Version = "v1";
document.Info.Description =
"Навчальний проєкт для демонстрації Scalar " +
"та Fluent OpenAPI metadata.";
document.Servers = new List<OpenApiServer>
{
new() { Url = "https://localhost:7234", Description = "HTTPS local" },
new() { Url = "http://localhost:5234", Description = "HTTP local" }
};
return Task.CompletedTask;
});
options.AddSchemaTransformer((schema, context, cancellationToken) =>
{
if (context.JsonTypeInfo.Type == typeof(decimal))
{
schema.Format = "decimal";
schema.Description = string.IsNullOrWhiteSpace(schema.Description)
? "Decimal monetary value."
: schema.Description + " Decimal monetary value.";
}
return Task.CompletedTask;
});
options.AddOperationTransformer((operation, context, cancellationToken) =>
{
operation.Responses.TryAdd("500", new OpenApiResponse
{
Description = "Unexpected server error"
});
return Task.CompletedTask;
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi("/openapi/{documentName}.json")
.AllowAnonymous();
app.MapScalarApiReference("/scalar", options =>
{
options.WithTitle("Minimal API Scalar Demo")
.WithOpenApiRoutePattern("/openapi/{documentName}.json")
.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient)
.ShowOperationId()
.ExpandAllTags()
.SortTagsAlphabetically()
.SortOperationsByMethod()
.PreserveSchemaPropertyOrder();
})
.AllowAnonymous();
}
app.MapGet("/_internal/ping", () => TypedResults.Ok(new
{
status = "ok",
serverTime = DateTimeOffset.UtcNow
}))
.ExcludeFromDescription();
app.MapProductEndpoints();
app.Run();
Тепер уже можна читати цей файл як коротку історію:
- Реєструємо store.
- Реєструємо OpenAPI generator.
- Додаємо transformers.
- У development відкриваємо
/openapi/...і/scalar. - Ховаємо технічний endpoint із документації.
- Реєструємо продуктову фічу.
Саме так і має виглядати Program.cs, коли проєкт уже зріс вище «Hello World».
15. Як перевірити, що все справді працює
15.1. Запускаємо
dotnet watch
15.2. Перевіряємо OpenAPI JSON
Відкрийте:
https://localhost:7234/openapi/v1.json
Якщо ви бачите JSON-документ, генерація OpenAPI працює.
15.3. Перевіряємо Scalar
Відкрийте:
https://localhost:7234/scalar
Там ви маєте побачити:
- групу
Products; - список із 5 операцій;
- summaries;
- operation id;
- request/response models;
- query і route parameters;
422на create/update;- hidden технічний endpoint, якого нема в документації.
15.4. Ручні запити для перевірки metadata
curl "https://localhost:7234/api/products?search=arabica&includeInactive=false"
curl -X POST "https://localhost:7234/api/products" \
-H "Content-Type: application/json" \
-d '{
"name": "Espresso Cup",
"price": 89.00,
"isActive": true
}'
curl -X POST "https://localhost:7234/api/products" \
-H "Content-Type: application/json" \
-d '{
"name": "",
"price": 0,
"isActive": true
}'
16. Додаткові сценарії конфігурації Scalar, які корисно знати
Не все треба вставляти одразу в навчальний проєкт, але важливо розуміти межі системи.
16.1. Власний маршрут для документації
app.MapScalarApiReference("/docs");
16.2. Кілька документів
У більш просунутих сценаріях Scalar може працювати не з одним, а з кількома OpenAPI-документами: наприклад, окремо для public, internal, admin. Точний fluent-синтаксис тут залежить від версії пакета Scalar.AspNetCore, тому перед реалізацією перевіряйте актуальний API саме вашої версії.
Інженерний принцип залишається тим самим:
- у вас є кілька
MapOpenApi(...)документів; - Scalar отримує інформацію, як між ними перемикатися;
- один із них робиться default.
16.3. Попередньо налаштований HTTP client для code samples
app.MapScalarApiReference(options =>
{
options.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient);
});
16.4. Dynamic configuration через HttpContext
app.MapScalarApiReference((options, httpContext) =>
{
options.WithTitle($"API docs for {httpContext.Request.Host}");
});
Це вже корисно в multi-tenant або role-aware сценаріях.
17. Типові помилки
Produces, Accepts, WithSummary, WithDescription, а лише потім ручні патчі.ExcludeFromDescription().WithTags, RequireAuthorization і shared rules, документація стає дубльованою, а код крихким.18. Практичні завдання
Рівень 1: Базовий
Відтворіть проєкт зі статті повністю. Досягніть стану, коли:
/openapi/v1.jsonвідкривається;/scalarвідкривається;/_internal/pingвідсутній у документації;POST /api/productsпоказує422у Scalar.
GET /api/products додайте ще один query-параметр sortBy, але без атрибутів. Опис параметра зробіть через WithOpenApi(...).Рівень 2: Логіка і metadata
Створіть нову групу /api/orders і опишіть її через той самий стиль:
WithTags("Orders")на групі;WithSummaryіWithDescriptionна кожному endpoint-і;Produces/ProducesProblem/ProducesValidationProblem;- мінімум один route parameter з описом через
WithOpenApi(...).
500 з AddOperationTransformer(...) у більш складний варіант: додавайте 500 лише тим операціям, які знаходяться під /api/.Рівень 3: Архітектура
Зробіть другу OpenAPI-конфігурацію, наприклад internal, і налаштуйте Scalar на роботу з двома документами:
v1;internal.
Після цього забезпечте, щоб internal був вибраний як default document.
/api/products і налаштуйте документацію так, щоб у Scalar було видно security requirements. Поясніть, яка частина конфігурації належить до ASP.NET authorization, а яка до OpenAPI/Scalar.19. Резюме
Scalar не генерує контракт
Fluent API цілком достатній
WithSummary, WithDescription, WithTags, Accepts, Produces, WithOpenApi, Route Groups і transformers.Route Group + transformers = масштабованість
Правильний фінал — /scalar
/scalar і бачите повний, охайний, перевіряємий API reference, значить ви не просто «підключили красивий UI», а правильно зібрали контракт свого Minimal API./openapi/v1.json, і фінально дивитесь очима споживача в /scalar. Саме так API-документація перестає бути формальністю і стає частиною розробки.Далі: якщо вам потрібен не сучасний Scalar-шлях, а окремий класичний стек з Swagger UI та Swashbuckle, переходьте до Swagger / Swashbuckle у Minimal API.
Структура проєкту: від хаосу до архітектури
Еволюція організації Minimal API проєкту: від одного файлу до Extension Methods, Route Groups, Feature Folders та Vertical Slice Architecture.
Swagger / Swashbuckle у Minimal API: окремий класичний шлях
Окремий матеріал про Swagger UI та Swashbuckle в ASP.NET Core Minimal API: коли використовувати, як підключити в .NET 8, як працює AddEndpointsApiExplorer, AddSwaggerGen, UseSwagger, UseSwaggerUI та як документувати endpoint-и.