minimal-apis について
Minimal APIs は、ASP.NET Core で軽量な HTTP API やマイクロサービスを構築するための開発スキルです。エンドポイントの整理、検証、型付きレスポンス、OpenAPI ドキュメントを扱います。
主な機能
- 01Minimal APIs によるエンドポイント作成
- 02ルートのグループ化と設定共有
- 03エンドポイントフィルターによる検証
- 04TypedResults による型付きレスポンス
- 05OpenAPI ドキュメントの生成
活用シーン
- 01軽量な HTTP API の構築
- 02関連するサービスエンドポイントの整理
- 03受信リクエストの検証
- 04OpenAPI による API ドキュメント作成
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ managedcode/ dotnet-skills --skill minimal-apisインストール後、Claude Code で/minimal-apisを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
manifest.jsonreferences/anti-patterns.mdpatterns.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 API | OData、JsonPatch |
| 軽量なハンドラー | 属性を多用する場合 |
| .NET 8 以降のプロジェクト | [ApiController] の機能が必要な場合 |
ワークフロー
- エンドポイントを Program.cs に直接定義する(小規模な API の場合)
- 関連するエンドポイントにはルートグループを使う
- API の成長に合わせてハンドラーを別のクラスに移す
- 横断的な関心事にはフィルターを適用する
- 型安全なレスポンスには TypedResults を使う
.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 仕様が正確である
- ルートグループが共通の構成を共有する
- ハンドラーがテスト可能である(依存関係をモックできる)
コメント
Markdown 対応