mcpField
W
web-api

web-api

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

web-api について

コントローラーベースの ASP.NET Core API の設計、レビュー、保守を支援するスキルです。明確な HTTP 契約とポリシーを重視し、Minimal APIs ではなくコントローラーを使うべき場面を判断できます。

主な機能

  1. 01コントローラー構成と薄いアクション
  2. 02明示的なモデルバインディングと DTO 境界
  3. 03バリデーションと一貫したエラー応答
  4. 04API バージョニングと例外処理

活用シーン

  1. 01既存の API コントローラーとフィルターのレビュー
  2. 02予測可能な HTTP 動作を持つ API の構築
  3. 03コントローラー内のビジネスロジックと永続化処理の削減

インストール

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

$npx skills add https://github.com/managedcode/dotnet-skills --skill web-api

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

同梱ファイル

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

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

詳細

ASP.NET Core Web API

対象となるケース

  • ASP.NET Core でコントローラーベースの API を開発している
  • コントローラー固有の拡張性や規約が必要
  • 既存の API コントローラーやフィルターを移行またはレビューしている

ワークフロー

  1. 古いテンプレートで使われていたという理由だけで選ぶのではなく、コントローラー中心の機能が API に必要な場合にコントローラーを使用します。
  2. コントローラーは薄く保ちます。HTTP に関する処理をアプリケーションサービスやハンドラーに渡し、アクションにデータアクセスやビジネスルールを直接埋め込まないようにします。
  3. 明確な DTO の境界、明示的な検証、予測可能な HTTP ステータスの振る舞いを維持します。
  4. API の公開範囲に意図しない不整合が生じないよう、コントローラーレベルとエンドポイントレベルの両方で認証と認可を確認します。
  5. OpenAPI の生成、バージョニング、エラー契約の振る舞いは、成り行き任せにせず意図的に設計します。
  6. 慣例でコントローラーを選ぶのではなく、新しいシンプルな API には minimal-apis を使用します。

現在のアップストリームに関する注意事項

  • dotnet/aspnetcore の v10.0.11 はサービス更新版であり、OpenAPI スタックを 2.7.5 に更新します。コントローラーベースの API に関する指針は、引き続き規約、高度なモデルバインディング、OData、JsonPatch、既存のフィルターに依存します。アップグレード後は、生成されたドキュメントとクライアント契約のチェックを再実行してください。
  • 2026 年 8 月の aspnetcore-10.0 の概要では、コントローラー API を置き換えるのではなく、Minimal API と並べて扱っています。公開 API 契約を変更する前に、ルーティング、OpenAPI、認証、ホスティングに関する専用ページを参照してください。

成果物

  • 明示的な契約とポリシーを備えたコントローラー API
  • コントローラーの肥大化の抑制
  • 重要な API の振る舞いに関するテストまたはスモークチェック

検証

  • コントローラーの機能が実際に必要とされている
  • アクションにビジネスロジックや永続化の詳細が隠されていない
  • エンドポイント間で HTTP のセマンティクスが予測可能に保たれている

コントローラーの構成

C# 12 以降のプライマリコンストラクターを依存性注入に使用し、コントローラーを HTTP に関する処理に集中させます。

[ApiController]
[Route("api/[controller]")]
public class OrdersController(
    IOrderService orderService,
    ILogger<OrdersController> logger) : ControllerBase
{
    [HttpGet("{id:guid}")]
    [ProducesResponseType<OrderDto>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetById(Guid id, CancellationToken ct)
    {
        var order = await orderService.GetByIdAsync(id, ct);
        return order is null ? NotFound() : Ok(order);
    }

    [HttpPost]
    [ProducesResponseType<OrderDto>(StatusCodes.Status201Created)]
    [ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> Create(CreateOrderRequest request, CancellationToken ct)
    {
        var order = await orderService.CreateAsync(request, ct);
        return CreatedAtAction(nameof(GetById), new { id = order.Id }, order);
    }
}

モデルバインディング

明確さのため、バインド元を明示的に宣言します。

[HttpGet("{id:guid}")]
public async Task<IActionResult> GetWithOptions(
    [FromRoute] Guid id,
    [FromQuery] bool includeDeleted = false,
    [FromHeader(Name = "X-Correlation-Id")] string? correlationId = null,
    CancellationToken ct = default)
{
    // Route: id, Query: includeDeleted, Header: X-Correlation-Id
}

リクエスト DTO には、必須メンバーを持つレコード型を使用します。

public record CreateProductRequest
{
    public required string Name { get; init; }
    public required decimal Price { get; init; }
    public string? Description { get; init; }
    public IReadOnlyList<string> Tags { get; init; } = [];
}

検証

複雑な検証ルールには FluentValidation を使用します。

public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
    public CreateOrderRequestValidator(IProductRepository products)
    {
        RuleFor(x => x.CustomerId)
            .NotEmpty()
            .WithMessage("Customer ID is required");

        RuleFor(x => x.Items)
            .NotEmpty()
            .WithMessage("Order must contain at least one item");

        RuleForEach(x => x.Items).ChildRules(item =>
        {
            item.RuleFor(i => i.ProductId)
                .NotEmpty()
                .MustAsync(async (id, ct) => await products.ExistsAsync(id, ct))
                .WithMessage("Product does not exist");

            item.RuleFor(i => i.Quantity)
                .GreaterThan(0)
                .LessThanOrEqualTo(100);
        });
    }
}

一貫性のある Problem Details レスポンスを構成します。

builder.Services.Configure<ApiBehaviorOptions>(options =>
{
    options.InvalidModelStateResponseFactory = context =>
    {
        var problemDetails = new ValidationProblemDetails(context.ModelState)
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            Title = "One or more validation errors occurred.",
            Status = StatusCodes.Status400BadRequest,
            Instance = context.HttpContext.Request.Path
        };

        return new BadRequestObjectResult(problemDetails);
    };
});

API バージョニング

URL パスによるバージョニングを構成します。

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0")]
public class ProductsV1Controller(IProductService productService) : ControllerBase
{
    [HttpGet("{id}")]
    public async Task<IActionResult> Get(int id, CancellationToken ct)
    {
        var product = await productService.GetAsync(id, ct);
        return Ok(product);
    }
}

例外処理

一貫性のあるエラーレスポンスを実現するため、グローバル例外ハンドラーを使用します。

public class GlobalExceptionHandler(
    ILogger<GlobalExceptionHandler> logger) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        logger.LogError(exception, "Unhandled exception occurred");

        var problemDetails = exception switch
        {
            ValidationException validationEx => new ProblemDetails
            {
                Status = StatusCodes.Status400BadRequest,
                Title = "Validation Error",
                Detail = validationEx.Message
            },
            NotFoundException notFoundEx => new ProblemDetails
            {
                Status = StatusCodes.Status404NotFound,
                Title = "Resource Not Found",
                Detail = notFoundEx.Message
            },
            _ => new ProblemDetails
            {
                Status = StatusCodes.Status500InternalServerError,
                Title = "Internal Server Error"
            }
        };

        problemDetails.Extensions["traceId"] = httpContext.TraceIdentifier;

        httpContext.Response.StatusCode = problemDetails.Status ?? 500;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}

参考資料

  • patterns.md - コントローラーのパターン、モデルバインディング、検証、バージョニング、レスポンス処理、フィルターのパターン
  • anti-patterns.md - 肥大化したコントローラー、一貫性のないエラー、キャンセルトークンの欠落、不適切な HTTP セマンティクスなど、避けるべき一般的な API の問題

コメント