Api contract review
Skill tunahanaliozturk/secure-dotnet-skills/skills/api-contract-review
Use when reviewing the design of a REST/HTTP API in ASP.NET Core — resource modeling, status codes, error shape, idempotency, versioning, pagination, and OpenAPI accuracy — before it ships.From its SKILL.md
npx -y skills add tunahanaliozturk/secure-dotnet-skills --skill api-contract-reviewAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
10.1 KB, ~2.3k tokens by cl100k_base, as published. Nobody here has run it
API Contract Review
Directs the agent to review the HTTP contract of an ASP.NET Core API end-to-end: verb semantics, status-code correctness, error shape, idempotency guarantees, concurrency, versioning strategy, pagination bounds, and whether the OpenAPI document faithfully describes the real responses — producing a concrete, API-named finding for every gap before the contract becomes load-bearing for clients.
When to use
- A new REST endpoint, controller, or minimal-API handler is being added or modified and the HTTP contract must be reviewed before client teams depend on it.
- A PR changes a response shape, status code, or route and backward-compatibility risk must be assessed.
- An API is about to ship to external consumers and the OpenAPI document needs to be validated against real behavior.
- A code review reveals ad-hoc error JSON, missing
Locationheaders on creates, or unbounded list endpoints.
Process
- Enumerate the resources and verbs. List every route, its HTTP method, and what resource it operates on. Confirm verb semantics: GET is safe and idempotent (no side effects), POST creates or triggers, PUT is a full idempotent replace, PATCH is a partial update, DELETE is idempotent.
- Check status-code correctness for every outcome. Map each success and error path to the correct status code:
201 Created+Locationon create,204 No Contenton empty success,400for malformed input,422for semantically invalid input,409for conflict,404vs403for missing vs forbidden,412for failed precondition. - Check the error contract. Confirm all error responses use
ProblemDetailsorValidationProblemDetails(RFC 7807) viaResults.Problem,Results.ValidationProblem, orAddProblemDetails. Flag any ad-hoc{ "error": "..." }bodies or non-standard error shapes. - Check idempotency and verb safety. For unsafe, non-idempotent POSTs that have real-world side effects (payments, orders, emails) confirm an
Idempotency-Keyrequest header is accepted and the server deduplicates replayed requests. Confirm PUT and DELETE operations are genuinely idempotent (repeated calls return the same result). - Check versioning, pagination, and content negotiation. Verify an explicit versioning strategy (
Asp.VersioningURL segment or header). Verify all list endpoints have a bounded page size, and return a cursor or offset with a documentednexttoken orLinkheader. Confirm theAcceptheader is honored and media types are consistent. - Check the OpenAPI document matches reality. Confirm every status code emitted by the handler is declared via
[ProducesResponseType](controllers) or.Produces<T>(statusCode)(minimal APIs), the document is generated withMicrosoft.AspNetCore.OpenApior Swashbuckle, and response schemas reference DTOs not EF entities. - Output findings with the concrete fix. For each gap, name the exact type, attribute, or method to apply. Re-check the same pattern in sibling handlers before closing.
.NET / Azure checks
- Verb semantics. GET must be safe and idempotent — no mutations, no visible side effects. POST creates a new resource or triggers a non-idempotent action. PUT performs a full, idempotent replace of a named resource (same outcome for repeated calls). PATCH applies a partial update via
JsonPatch(Microsoft.AspNetCore.JsonPatch) or JSON merge-patch (Content-Type: application/merge-patch+json). DELETE is idempotent — deleting an already-deleted resource must return204or404, not500. - Status codes.
201 Createdwith aLocation: /resource/{id}header on every successful POST that creates a resource.204 No Contenton mutations with no body to return.400 Bad Requestfor syntactically malformed input (unparseable JSON, wrong content-type, missing required header).422 Unprocessable Entityfor input that is well-formed but semantically invalid (a date range where end < start, a reference to a non-existent foreign key).409 Conflictfor state conflicts (optimistic-concurrency collisions, duplicate resource creation).404 Not Foundwhen the resource does not exist;403 Forbiddenwhen it exists but the caller lacks permission.412 Precondition FailedwhenIf-Matchdoes not match the current ETag. - Error contract — RFC 7807 ProblemDetails. All error responses must conform to RFC 7807:
Content-Type: application/problem+json, fieldstype,title,status,detail,instance. UseResults.Problem(detail, statusCode: 400)(minimal APIs) orreturn Problem(detail: ..., statusCode: 400)(controllers). For validation errors, useResults.ValidationProblem(errors)orreturn ValidationProblem(ModelState)— this returnsValidationProblemDetailswith anerrorsdictionary, status422, and the correct content type. Registerbuilder.Services.AddProblemDetails()to get a consistent default problem response for unhandled exceptions. Never return{ "error": "..." },{ "message": "..." }, or any other ad-hoc JSON shape on error. - Idempotency-Key for non-idempotent POSTs. Any POST that charges a payment, creates an order, sends a message, or otherwise has an irreversible side effect must accept an
Idempotency-Key: <uuid>request header. The server stores the key and the response; repeated requests with the same key return the cached response without re-executing the side effect. Clients must be able to safely retry on network errors. Without this, a transient failure during a payment POST causes a double-charge. - Optimistic concurrency with ETag + If-Match. Resources that can be concurrently updated must emit an
ETagresponse header (a version hash or row-version value). Update operations (PUT/PATCH) must require the client to sendIf-Match: "<etag>". If the stored version does not match, return412 Precondition Failed(not409). This prevents a lost-update race between concurrent writers. In ASP.NET Core, readRequest.Headers.IfMatchand compare againstentry.RowVersionor a computed hash. - API versioning via Asp.Versioning. Every public API route must be versioned. Use the
Asp.Versioning.MvcNuGet package (andAsp.Versioning.Mvc.ApiExplorerfor OpenAPI explorer integration), namespaceAsp.Versioning. Prefer URL-segment versioning (/v{version:apiVersion}/) for public APIs; header versioning (api-version: 2.0) for internal or partner APIs. Declare versions on controllers with[ApiVersion("1.0")]and deprecate old versions with[ApiVersion("1.0", Deprecated = true)]. Making a breaking change on an unversioned route is never acceptable. - Bounded pagination. No list endpoint may return an unbounded collection. Require
pageSize(orlimit) with a maximum cap enforced server-side (e.g.Math.Min(pageSize, 100)). For offset-based pagination return{ "items": [...], "nextPage": "/orders?skip=20&limit=20" }; for cursor-based return an opaquenextCursortoken. Document thenexttoken orLink: <url>; rel="next"header in the OpenAPI spec. - OpenAPI document accuracy. Generate the document with
Microsoft.AspNetCore.OpenApi(builder.Services.AddOpenApi(),app.MapOpenApi()) or Swashbuckle (builder.Services.AddSwaggerGen()). Every handler must declare[ProducesResponseType<CreateOrderResponse>(StatusCodes.Status201Created)],[ProducesResponseType<ValidationProblemDetails>(StatusCodes.Status422UnprocessableEntity)], etc. Response schemas must reference DTOs, not EF entity classes. Undocumented status codes confuse client code generators and SDK authors.
Red flags
| Signal | Why it matters |
|---|---|
200 OK returned with an error body (e.g. { "success": false, "error": "..." }) | Clients cannot distinguish success from failure by status code; HTTP semantics are broken and SDK generators produce incorrect code. |
POST /orders returns 200 with no Location header on success | Violates RFC 7231 §6.3.2; the caller has no reliable way to retrieve the created resource without parsing the body or issuing a second query. |
An error response with Content-Type: application/json and a plain { "error": "..." } body | Not RFC 7807; different endpoints expose different error shapes, making client error-handling inconsistent and fragile. |
A list endpoint with no pageSize parameter or no server-side cap | A single request can return millions of rows; causes OOM on the server and a large, slow payload for the client. |
| An unversioned public route accepting breaking changes in-place | Any client that has not opted in to the new behavior breaks silently; there is no way to communicate the change or deprecate safely. |
PUT /resource/{id} used for partial updates instead of PATCH | PUT semantics require a full replace; sending a partial body causes unset fields to be nulled out, silently corrupting data. |
EF entity class (e.g. Order, ApplicationUser) returned directly as the response DTO | Exposes server-managed columns (RowVersion, PasswordHash, IsDeleted, foreign-key navigations) and couples the wire contract to the database schema. |
422 status code undeclared in the OpenAPI document | Code generators emit no error type for validation failures; client developers discover the shape at runtime from an unexpected response. |
Example
See examples/api-contract-review/ and the full before/after walkthrough in examples/api-contract-review/README.md.
Related skills
- design-dotnet-feature — use first to validate the feature design and resource model before reviewing the HTTP contract in detail.
- auth-flow-review — review authorization on every endpoint produced by this contract: scopes, policies, and default-deny posture.
- rate-limiting-review — once the contract is correct, review 429 semantics and Retry-After header contract for protected endpoints.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.