api-design
Design the HTTP contract of an ASP.NET Core API before implementing it - resource naming, methods and status codes, pagination, filtering, ProblemDetails errors, versioning, rate limiting, and idempot
SKILL.mdtemplate/.claude/skills/api-design/SKILL.md
GitHubAPI Design (ASP.NET Core)
Decide the contract first, write it down in the plan or PR, then implement it with the dotnet-webapi skill. Follow the conventions already in the codebase when they exist; use these defaults when they don't.
Resources and URLs
- Plural nouns for collections, ids for items:
/orders,/orders/{id}. - Nest only for true ownership, at most one level:
/orders/{id}/items. - Lowercase, kebab-case path segments; camelCase JSON properties (the System.Text.Json web default).
- Actions that don't map to CRUD become sub-resources with POST:
POST /orders/{id}/cancel. - Route constraints on ids:
/orders/{id:guid}or{id:int}.
Methods and status codes
| Operation | Method | Success | Common failures |
|---|---|---|---|
| List | GET | 200 | 400 invalid query |
| Get one | GET | 200 | 404 |
| Create | POST | 201 + Location header + body |
400, 409 conflict, 422 business rule |
| Replace | PUT | 200 with body, or 204 | 400, 404, 409 |
| Partial update | PATCH | 200 with body | 400, 404, 409 |
| Delete | DELETE | 204 | 404 (or 204 if already gone, pick one and be consistent) |
- 401 when unauthenticated, 403 when authenticated but not allowed. Return 404 instead of 403 when revealing that the resource exists is itself a leak.
- Use typed results (
TypedResults.Ok,TypedResults.Created,TypedResults.NotFound) so status codes appear in OpenAPI.
Request and response models
- Separate request and response DTOs from EF entities; never bind or return entities directly.
- Requests contain only client-settable fields: no ids, timestamps, or computed values.
- Use
DateTimeOffsetin UTC with ISO 8601 for timestamps; decimals for money, neverdouble. - Enums as strings (
JsonStringEnumConverter) so adding values doesn't break clients reading numbers.
Errors
- Use
ProblemDetails(RFC 9457) for every error:builder.Services.AddProblemDetails(),TypedResults.Problem(...), andTypedResults.ValidationProblem(errors)for 400s with field errors. - Keep
titlestable per error type; put specifics indetail. Add machine-readable codes as an extension ("code": "order.cart_empty"). - Never return exception messages or stack traces outside Development.
Pagination, filtering, sorting
- Every list endpoint is paginated with a maximum page size; never return an unbounded collection.
- Offset paging (
?page=2&pageSize=20) for small, stable admin lists. Cursor paging (?cursor=...&limit=20) for large or frequently changing data and infinite scroll. - Response envelope for lists:
{ "items": [...], "nextCursor": "..." }or{ "items": [...], "page": 2, "pageSize": 20, "totalCount": 135 }. Only computetotalCountwhen the UI needs it; it costs a second query. - Filters as query parameters named after response fields (
?status=paid&customerId=...); sorting as?sort=createdAt/?sort=-createdAt, with an allowlist of sortable fields.
Versioning
- Don't version until there is a breaking change you can't avoid. Additive changes (new optional fields, new endpoints) are not breaking.
- When needed, use
Asp.Versioning.Http(minimal APIs) orAsp.Versioning.Mvc(controllers) with URL segment versioning (/v1/orders), and keep the previous version until clients have moved. - Breaking: removing or renaming fields, changing types or meaning, new required request fields, changing status codes clients depend on.
Rate limiting
- Use the built-in middleware:
builder.Services.AddRateLimiter(...),app.UseRateLimiter(), and.RequireRateLimiting("policy")per endpoint group. - Set
RejectionStatusCode = StatusCodes.Status429TooManyRequests; the default is 503. - Partition by user id when authenticated, by client IP otherwise. Stricter policies on auth and expensive endpoints.
Idempotency and concurrency
- GET, PUT, and DELETE must be idempotent. For POSTs that create money-moving or otherwise non-repeatable effects, accept an
Idempotency-Keyheader and return the stored result on retries. - Prevent lost updates with optimistic concurrency: an EF Core concurrency token surfaced as an
ETag, checked viaIf-Match, returning 412 on mismatch (or 409 if not using ETags).
Checklist before implementing
- URLs, methods, and status codes for every endpoint
- Request/response DTOs, with validation rules
- Error cases and their ProblemDetails shape
- Pagination and filters for every list
- Auth requirement per endpoint
- Breaking-change check against existing clients