mcpField
M
minimal-api

minimal-api

github.com/codewithmukesh開発7913.0k 回インストール12 週間前に更新
リポジトリ

minimal-api について

エンドポイントの構成、型安全なレスポンス、API ドキュメントを扱う、.NET 10 向け Minimal APIs ガイドです。エンドポイントグループの自動検出により、Program.cs で個別登録せずに追加できます。

主な機能

  1. 01エンドポイントグループの自動検出
  2. 02TypedResults による型安全な応答
  3. 03パラメーターのバインドパターン
  4. 04エンドポイントフィルター
  5. 05組み込み OpenAPI 設定
  6. 06レート制限と出力キャッシュ

活用シーン

  1. 01新しい HTTP API の構築
  2. 02機能ごとの API エンドポイント整理
  3. 03型情報付き OpenAPI ドキュメントの生成
  4. 04エンドポイントへの検証やレート制限の追加

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill minimal-api

インストール後、Claude Code で/minimal-apiを入力して読み込まれたか確認します。

詳細

Minimal API(.NET 10)

基本原則

  1. Minimal API をデフォルトにする — レガシーコードを移行する場合に限り、コントローラーを使用します。Minimal API は軽量で高速であり、どのようなアーキテクチャスタイルとも相性よく組み合わせられます。
  2. エンドポイントは MapGroup でグループ化する — Program.cs に個別の MapGet/MapPost 呼び出しを散在させないでください。関連するエンドポイントをまとめてグループ化します。
  3. OpenAPI には TypedResults を使用する — TypedResults.Ok(value) を使うと、コンパイル時の型安全性と正確な OpenAPI ドキュメントを両方得られます。Results.Ok(value) では得られません。
  4. コメントではなくメタデータを使う — エンドポイントの説明には .WithName()、.WithTags()、.WithSummary() を使用します。メタデータは OpenAPI 仕様に反映されます。

パターン

エンドポイントグループの自動検出(必須パターン)

各エンドポイントグループは専用のファイルに配置し、IEndpointGroup を実装します。Program.cs で app.MapEndpoints() を一度呼び出すと、すべてのグループが自動的に検出され、登録されます。新しいエンドポイントグループを追加しても、Program.cs を変更する必要はありません。

// Extensions/IEndpointGroup.cs
public interface IEndpointGroup
{
    void Map(IEndpointRouteBuilder app);
}
// Extensions/EndpointExtensions.cs
public static class EndpointExtensions
{
    public static WebApplication MapEndpoints(this WebApplication app)
    {
        var groups = typeof(Program).Assembly
            .GetTypes()
            .Where(t => t.IsAssignableTo(typeof(IEndpointGroup)) && !t.IsInterface && !t.IsAbstract)
            .Select(Activator.CreateInstance)
            .Cast<IEndpointGroup>();

        foreach (var group in groups)
            group.Map(app);

        return app;
    }
}
// Program.cs — this NEVER changes when adding endpoints
var app = builder.Build();
app.MapEndpoints();
app.Run();
// Features/Orders/OrderEndpoints.cs — one file per endpoint group
public sealed class OrderEndpoints : IEndpointGroup
{
    public void Map(IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/orders").WithTags("Orders");

        group.MapPost("/", CreateOrder)
            .WithName("CreateOrder")
            .WithSummary("Create a new order")
            .Produces<OrderResponse>(StatusCodes.Status201Created)
            .ProducesValidationProblem()
            .RequireAuthorization();

        group.MapGet("/{id:guid}", GetOrder)
            .WithName("GetOrder")
            .Produces<OrderResponse>()
            .ProducesProblem(StatusCodes.Status404NotFound);

        group.MapGet("/", ListOrders)
            .WithName("ListOrders")
            .Produces<PagedList<OrderResponse>>();
    }

    private static async Task<Results<Created<OrderResponse>, ValidationProblem>> CreateOrder(
        CreateOrderRequest request,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(new CreateOrder.Command(request.CustomerId, request.Items), ct);
        return result.IsSuccess
            ? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
            : TypedResults.ValidationProblem(result.Errors);
    }

    private static async Task<Results<Ok<OrderResponse>, NotFound>> GetOrder(
        Guid id,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(new GetOrder.Query(id), ct);
        return result.IsSuccess
            ? TypedResults.Ok(result.Value)
            : TypedResults.NotFound();
    }

    private static async Task<Ok<PagedList<OrderResponse>>> ListOrders(
        [AsParameters] ListOrdersQuery query,
        ISender sender,
        CancellationToken ct)
    {
        var result = await sender.Send(query, ct);
        return TypedResults.Ok(result);
    }
}
型安全なレスポンスのための TypedResults

TypedResults はコンパイル時の保証と OpenAPI スキーマの自動生成を提供します。

// GOOD — TypedResults with union return type
private static async Task<Results<Ok<Product>, NotFound, ValidationProblem>> GetProduct(
    Guid id,
    AppDbContext db,
    CancellationToken ct)
{
    var product = await db.Products.FindAsync([id], ct);
    return product is not null
        ? TypedResults.Ok(product)
        : TypedResults.NotFound();
}
パラメーターのバインド

.NET 10 の Minimal API は、ルート、クエリ、ヘッダー、ボディ、DI からパラメーターを自動的にバインドします。

// Route parameters
app.MapGet("/orders/{id:guid}", (Guid id) => ...);

// Query parameters (nullable = optional)
app.MapGet("/orders", (int page, int? pageSize, string? status) => ...);

// Complex query parameters with [AsParameters]
public record ListOrdersQuery(int Page = 1, int PageSize = 20, string? Status = null);
app.MapGet("/orders", ([AsParameters] ListOrdersQuery query) => ...);

// Header binding
app.MapGet("/orders", ([FromHeader(Name = "X-Correlation-Id")] string? correlationId) => ...);

// DI services are auto-resolved (no attribute needed)
app.MapPost("/orders", (CreateOrderRequest request, ISender sender) => ...);
エンドポイントフィルター

フィルターは Minimal API におけるアクションフィルターに相当します。検証、ロギング、冪等性チェックなど、横断的関心事に使用します。

標準の ValidationFilter<TRequest> 実装(FluentValidation を使用し、バリデーターを DI から解決し、登録されていない場合は適切にスキップするもの)は、error-handling スキルにあります。プロジェクトごとに再実装せず、そちらを使用してください。

// Apply the canonical filter (see error-handling skill) to a mutating endpoint
group.MapPost("/", CreateOrder)
    .AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();

// Apply a filter to a group (affects all endpoints in the group)
group.AddEndpointFilter<LoggingFilter>();
OpenAPI/Swagger の設定

.NET 10 には OpenAPI サポートが組み込まれています。Swashbuckle の代わりに使用してください。

// Program.cs — service registration only, no endpoint wiring
builder.Services.AddOpenApi();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();
}
app.MapEndpoints(); // auto-discovers all IEndpointGroup implementations

// Endpoint metadata enriches the OpenAPI spec
group.MapPost("/", CreateOrder)
    .WithName("CreateOrder")
    .WithSummary("Create a new order")
    .WithDescription("Creates a new order for the specified customer with the given line items.")
    .Produces<OrderResponse>(StatusCodes.Status201Created)
    .ProducesValidationProblem()
    .ProducesProblem(StatusCodes.Status500InternalServerError);
レート制限
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter("api", opt =>
    {
        opt.PermitLimit = 100;
        opt.Window = TimeSpan.FromMinutes(1);
    });
});

// Apply inside an IEndpointGroup.Map method
var group = app.MapGroup("/api/orders")
    .WithTags("Orders")
    .RequireRateLimiting("api");
出力キャッシュ
builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(5)));
    options.AddPolicy("ByIdCache", builder => builder
        .Expire(TimeSpan.FromMinutes(10))
        .SetVaryByRouteValue("id"));
});

group.MapGet("/{id:guid}", GetOrder)
    .CacheOutput("ByIdCache");

アンチパターン

エンドポイントを Program.cs に配置しない
// BAD — endpoints scattered in Program.cs
app.MapGet("/orders", async (AppDbContext db) => await db.Orders.ToListAsync());
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) => await db.Orders.FindAsync(id));
app.MapPost("/orders", async (Order order, AppDbContext db) => { /* ... */ });
app.MapGet("/products", async (AppDbContext db) => await db.Products.ToListAsync());

// ALSO BAD — manual MapGroup calls in Program.cs (grows with every feature)
app.MapGroup("/api/orders").WithTags("Orders").MapOrderEndpoints();
app.MapGroup("/api/products").WithTags("Products").MapProductEndpoints();
app.MapGroup("/api/customers").WithTags("Customers").MapCustomerEndpoints();
// Program.cs grows every time you add a feature...

// GOOD — auto-discovered, Program.cs never changes
app.MapEndpoints(); // discovers all IEndpointGroup implementations
型指定のない結果を使用しない
// BAD — Results.Ok doesn't contribute to OpenAPI schema
private static async Task<IResult> GetOrder(Guid id, AppDbContext db)
{
    var order = await db.Orders.FindAsync(id);
    return order is not null ? Results.Ok(order) : Results.NotFound();
}

// GOOD — TypedResults with explicit union type
private static async Task<Results<Ok<Order>, NotFound>> GetOrder(Guid id, AppDbContext db)
{
    var order = await db.Orders.FindAsync(id);
    return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
}
ドメインエンティティを直接返さない
// BAD — leaks internal structure, can't evolve independently
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
    await db.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id));

// GOOD — map to a response DTO
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
{
    var order = await db.Orders
        .Where(o => o.Id == id)
        .Select(o => new OrderResponse(o.Id, o.Total, o.CreatedAt))
        .FirstOrDefaultAsync();
    return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
});

選択ガイド

シナリオ推奨事項
新しい HTTP API機能ごとに IEndpointGroup を作成し、app.MapEndpoints() で自動検出する
既存の MVC プロジェクトコントローラーを維持し、段階的に移行する
OpenAPI ドキュメントTypedResults + .WithName() + .WithSummary() を使用する
リクエストの検証FluentValidation を使ったエンドポイントフィルター
認証/認可グループまたはエンドポイントに .RequireAuthorization("PolicyName") を設定する
レート制限AddRateLimiter + .RequireRateLimiting()
レスポンスキャッシュAddOutputCache + .CacheOutput()
複雑なモデルバインドレコード型と [AsParameters] を使用する

コメント