mcpField
M
minimal-apis

minimal-apis

github.com/managedcode開発49684 回インストール今日更新
リポジトリ

minimal-apis について

Minimal APIs は、ASP.NET Core で軽量な HTTP API やマイクロサービスを構築するための開発スキルです。エンドポイントの整理、検証、型付きレスポンス、OpenAPI ドキュメントを扱います。

主な機能

  1. 01Minimal APIs によるエンドポイント作成
  2. 02ルートのグループ化と設定共有
  3. 03エンドポイントフィルターによる検証
  4. 04TypedResults による型付きレスポンス
  5. 05OpenAPI ドキュメントの生成

活用シーン

  1. 01軽量な HTTP API の構築
  2. 02関連するサービスエンドポイントの整理
  3. 03受信リクエストの検証
  4. 04OpenAPI による API ドキュメント作成

インストール

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

$npx skills add https://github.com/managedcode/dotnet-skills --skill minimal-apis

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

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

manifest.json
references/
anti-patterns.md
patterns.md

詳細

Minimal API

対象となる場面

  • ASP.NET Core で新しい HTTP API を構築する
  • 軽量なマイクロサービスを作成する
  • Minimal API とコントローラーのどちらを選ぶか検討する
  • ルートグループを使ってエンドポイントを整理する
  • 検証とフィルターを実装する

ドキュメント

参考資料
  • patterns.md - ルートグループ、フィルター、TypedResults のパターン、パラメーターのバインド、エラー処理、テストの詳細
  • anti-patterns.md - 避けるべき Minimal API のよくある誤り

Minimal API とコントローラーの使い分け

Minimal API を使う場合コントローラーを使う場合
新規プロジェクト既存の MVC/API プロジェクト
マイクロサービス複雑なモデルバインド
シンプルな CRUD APIOData、JsonPatch
軽量なハンドラー属性を多用する場合
.NET 8 以降のプロジェクト[ApiController] の機能が必要な場合

ワークフロー

  1. エンドポイントを Program.cs に直接定義する(小規模な API の場合)
  2. 関連するエンドポイントにはルートグループを使う
  3. API の成長に合わせてハンドラーを別のクラスに移す
  4. 横断的な関心事にはフィルターを適用する
  5. 型安全なレスポンスには TypedResults を使う
  6. .WithOpenApi() を使って OpenAPI ドキュメントを生成する

最新の上流情報

  • dotnet/aspnetcore の v10.0.11 はサービスリリースです。Minimal API のルートグループ、フィルター、TypedResults のモデルに変更はありませんが、OpenAPI スタックが 2.7.5 に更新されています。アップグレード後は、ドキュメントの生成とコントラクトのスナップショットを再実行してください。
  • 2026 年 8 月の aspnetcore-10.0 の概要でも、軽量な HTTP API の参照先は引き続きこちらです。OpenAPI、フィルター、パラメーターのバインドに関する正確な動作を確認する場合は、専用の Minimal API ページを参照してください。

基本的なパターン

シンプルなエンドポイント
var app = builder.Build();

app.MapGet("/", () => "Hello World");

app.MapGet("/products/{id}", (int id) => Results.Ok(new { Id = id }));

app.MapPost("/products", (Product product) => Results.Created($"/products/{product.Id}", product));
TypedResults(強い型付け)
app.MapGet("/products/{id}", Results<Ok<Product>, NotFound> (int id, AppDb db) =>
{
    var product = db.Products.Find(id);
    return product is not null
        ? TypedResults.Ok(product)
        : TypedResults.NotFound();
});
依存性注入
app.MapGet("/products", async (IProductService service) =>
{
    return await service.GetAllAsync();
});

// Or with [FromServices] for clarity
app.MapGet("/products", async ([FromServices] IProductService service) =>
    await service.GetAllAsync());

ルートグループ

基本的なグループ化
var products = app.MapGroup("/api/products");

products.MapGet("/", GetAll);
products.MapGet("/{id}", GetById);
products.MapPost("/", Create);
products.MapPut("/{id}", Update);
products.MapDelete("/{id}", Delete);
共通の構成を持つグループ
var api = app.MapGroup("/api")
    .RequireAuthorization()
    .AddEndpointFilter<ValidationFilter>();

var products = api.MapGroup("/products")
    .WithTags("Products");

var orders = api.MapGroup("/orders")
    .WithTags("Orders")
    .RequireAuthorization("AdminOnly");

エンドポイントフィルター

インラインフィルター
app.MapGet("/products/{id}", (int id) => Results.Ok(id))
    .AddEndpointFilter(async (context, next) =>
    {
        var id = context.GetArgument<int>(0);
        if (id <= 0)
            return Results.BadRequest("Invalid ID");

        return await next(context);
    });
クラスベースのフィルター
public class ValidationFilter<T> : IEndpointFilter where T : class
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context,
        EndpointFilterDelegate next)
    {
        var argument = context.Arguments
            .OfType<T>()
            .FirstOrDefault();

        if (argument is null)
            return Results.BadRequest("Invalid request body");

        var validator = context.HttpContext.RequestServices
            .GetService<IValidator<T>>();

        if (validator is not null)
        {
            var result = await validator.ValidateAsync(argument);
            if (!result.IsValid)
                return Results.ValidationProblem(result.ToDictionary());
        }

        return await next(context);
    }
}

// Usage
products.MapPost("/", Create)
    .AddEndpointFilter<ValidationFilter<CreateProductRequest>>();
ルートグループのルートを介したグローバルフィルター
// All endpoints inherit filters from root group
var root = app.MapGroup("")
    .AddEndpointFilter<LoggingFilter>()
    .AddEndpointFilter<ErrorHandlingFilter>();

root.MapGet("/health", () => Results.Ok());
root.MapGroup("/api/products").MapGet("/", GetProducts);

大規模な API の整理

拡張メソッドパターン
// ProductEndpoints.cs
public static class ProductEndpoints
{
    public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/products")
            .WithTags("Products");

        group.MapGet("/", GetAll);
        group.MapGet("/{id}", GetById);
        group.MapPost("/", Create);

        return group;
    }

    private static async Task<Ok<List<Product>>> GetAll(IProductService service)
        => TypedResults.Ok(await service.GetAllAsync());

    private static async Task<Results<Ok<Product>, NotFound>> GetById(
        int id, IProductService service)
    {
        var product = await service.GetByIdAsync(id);
        return product is not null
            ? TypedResults.Ok(product)
            : TypedResults.NotFound();
    }

    private static async Task<Created<Product>> Create(
        CreateProductRequest request, IProductService service)
    {
        var product = await service.CreateAsync(request);
        return TypedResults.Created($"/api/products/{product.Id}", product);
    }
}

// Program.cs
app.MapProductEndpoints();
app.MapOrderEndpoints();

リクエスト/レスポンス DTO

// Separate from domain models
public record CreateProductRequest(string Name, decimal Price);
public record UpdateProductRequest(string Name, decimal Price);
public record ProductResponse(int Id, string Name, decimal Price);

// Don't expose domain entities directly
app.MapPost("/products", (CreateProductRequest request, IMapper mapper) =>
{
    var product = mapper.Map<Product>(request);
    // ...
    return TypedResults.Created($"/products/{product.Id}",
        mapper.Map<ProductResponse>(product));
});

避けるべきアンチパターン

アンチパターン問題点より良い方法
すべてを Program.cs に記述する保守が困難拡張メソッドを使う
ルートグループを使わない構成が重複する関連するエンドポイントをグループ化する
手動で検証するエラーが発生しやすいフィルターと FluentValidation を使う
エンティティを公開する密結合になるDTO を使う
TypedResults を使わないコンパイル時チェックがないTypedResults を使う
OpenAPI を無視するドキュメントがない.WithOpenApi() を追加する

OpenAPI の統合

builder.Services.AddOpenApi();

app.MapOpenApi();  // Serves OpenAPI spec

app.MapGet("/products", GetProducts)
    .WithName("GetProducts")
    .WithSummary("Get all products")
    .WithDescription("Returns a list of all available products")
    .Produces<List<Product>>(StatusCodes.Status200OK)
    .ProducesProblem(StatusCodes.Status500InternalServerError);

提供するもの

  • クリーンで整理された Minimal API エンドポイント
  • ルートグループとフィルターの適切な使用
  • TypedResults による型安全なレスポンス
  • OpenAPI ドキュメント
  • エンドポイントフィルターによる検証

検証

  • エンドポイントが正しいステータスコードを返す
  • 検証フィルターが無効な入力を検出する
  • OpenAPI 仕様が正確である
  • ルートグループが共通の構成を共有する
  • ハンドラーがテスト可能である(依存関係をモックできる)

コメント