Первый эндпоинт на Minimal API

Minimal API: первый рабочий эндпоинт за пять строк и аналогия конвейера на Python.

Суть: Minimal API — это способ описывать HTTP-эндпоинты прямо в Program.cs, без контроллеров. Идеален для небольших сервисов, микросервисов и обучения: меньше церемоний, выше производительность.

Minimal API хорош для маленьких сервисов, но в большом приложении начиная с .NET 6 появились Minimal API: вы пишете app.MapGet("/path", () => "результат") — и эндпоинт готов. В .NET 8/9 Minimal API дозрели до продакшена: есть валидация, типизированные результаты, группировка маршрутов.

Первый эндпоинт

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Привет, мир!");

app.MapGet("/users/{id}", (int id) =>
    new { Id = id, Name = "Аня" });

app.Run();

Первый эндпоинт на корне возвращает строку. Второй принимает id из URL (model binding сам преобразует строку в int) и возвращает объект — ASP.NET Core автоматически сериализует его в JSON. Запустите dotnet run и откройте /users/5 — получите {"id":5,"name":"Аня"}.

HTTP-методы

Каждому HTTP-глаголу — свой Map-метод: MapGet (чтение), MapPost (создание), MapPut (полное обновление), MapDelete (удаление). Это основа REST, к которой мы вернёмся в разделе про Web API.

Как работает под капотом

Когда приходит запрос GET /users/5, система маршрутизации сопоставляет путь с шаблоном /users/{id}, извлекает id = "5", преобразует в int и вызывает вашу функцию-обработчик. Возвращённый объект проходит через сериализатор System.Text.Json и уходит клиенту с заголовком Content-Type: application/json. Концептуально конвейер обработки — это цепочка функций, каждая из которых может что-то сделать с запросом и передать дальше. Смоделируем эту идею на Python.

# Аналог конвейера middleware: цепочка обработчиков запроса
def logging(request, nxt):
    print(f"-> {request['method']} {request['path']}")
    response = nxt(request)
    print(f"<- {response['status']}")
    return response

def routing(request, nxt):
    if request["path"] == "/users/5":
        return {"status": 200, "body": {"id": 5, "name": "Аня"}}
    return {"status": 404, "body": "Not Found"}

def build_pipeline(middlewares, final):
    def chain(i):
        if i >= len(middlewares):
            return final
        return lambda req: middlewares[i](req, chain(i + 1))
    return chain(0)

pipeline = build_pipeline([logging], routing)
print(pipeline({"method": "GET", "path": "/users/5"}))

Попробуй сам ▶ — здесь logging оборачивает routing: сначала логирует запрос, потом передаёт дальше, потом логирует ответ. Ровно так в ASP.NET Core middleware вызывают next().

Частые ошибки

  • Возвращать строку там, где нужен статус-код. Для контроля кода ответа используйте Results.Ok(), Results.NotFound(), Results.Created().
  • Логика прямо в лямбде растёт до сотен строк. Выносите обработчики в отдельные методы/классы, когда они усложняются.
  • Путать порядок параметров и источников. По умолчанию простые типы берутся из маршрута/строки запроса, сложные — из тела.

Best practices

  • Группируйте связанные маршруты через app.MapGroup("/users") — чище и можно навесить общие фильтры.
  • Возвращайте типизированные Results для корректных HTTP-кодов.
  • Minimal API — для небольших сервисов; для крупных с множеством действий удобнее контроллеры (следующий раздел).

Типизированные результаты и группировка

Возврат «голой» строки или объекта удобен для старта, но в реальном API лучше использовать Results и TypedResults: Results.Ok(dto), Results.NotFound(), Results.Created(uri, dto). Они задают корректный статус-код и одновременно несут информацию о типе — её подхватывает генератор OpenAPI, и документация получается точнее. Для группы связанных маршрутов есть MapGroup: вы пишете var users = app.MapGroup("/users") и дальше навешиваете эндпоинты и общие фильтры на эту группу.

В Minimal API параметры обработчика тоже разрешаются через DI: если в лямбду добавить параметр AppDbContext db или IUserService svc, контейнер подставит их автоматически, как и в контроллерах. Это стирает границу между «лёгким» Minimal API и «полноценными» контроллерами с точки зрения возможностей — отличается в основном стиль организации кода.

Minimal API против контроллеров: когда что

Оба подхода в .NET 8/9 production-ready и могут сосуществовать в одном проекте. Эмпирическое правило такое: Minimal API хорош для небольших и средних сервисов, BFF (backend-for-frontend), вебхуков, где ценится скорость и минимум церемоний. Контроллеры выигрывают в крупных приложениях с десятками действий, где важны единая структура, фильтры, конвенции и привычная организация по папкам. Выбор — вопрос масштаба и вкуса команды, а не «нового против старого».

Запускаемая врезка выше показала важную идею, которая красной нитью пройдёт через весь курс: обработка запроса — это цепочка обёрток. Каждое звено получает запрос, решает, что с ним сделать, и передаёт дальше через next. Эту же модель вы увидите в middleware, в фильтрах и в обработчиках авторизации — поняв её один раз, вы поймёте половину фреймворка.

Итог: Minimal API позволяет поднять рабочий эндпоинт за минуты. Вы увидели, как конвейер передаёт запрос по цепочке — эту модель мы углубим дальше.

Проверьте себя
1. Что делает app.MapGet с шаблоном /users/{id} и параметром int id?
AСоздаёт POST-эндпоинт
BРегистрирует GET-эндпоинт, где id из URL автоматически преобразуется в int
CУдаляет пользователя
DСоздаёт контроллер
2. Что произойдёт, если вернуть из обработчика объект (например анонимный тип)?
AБудет ошибка
BВернётся пустой ответ
CASP.NET Core автоматически сериализует его в JSON
DВернётся XML по умолчанию