Type to search pages and rules.

Browse pages

HTTP API

The WebApi layer is the thin HTTP boundary of a LiteSpec backend: it converts each HTTP request into a message for the Application layer, dispatches it, and maps the result back to a stable HTTP response. This page states what one endpoint owns, how routes, errors, and collection paging behave, and how the OpenAPI contract, the machine-readable description of the API, is generated from the code. Keeping business rules, storage, and provider calls out of endpoints keeps HTTP replaceable transport, so the API evolves without touching business behavior.

Markdown43 provisions

Stack: any Layer: web-api

Concepts#

Endpoints as Transport Adapters#

An endpoint does three things and nothing else: it converts HTTP input to an Application message, dispatches that message through its mediator, and maps the result to the HTTP response. One endpoint class serves one route operation and sits in its use case folder, and the host maps the endpoints through the one public assembly marker. Business rules, persistence access, provider calls, and error mapping all live outside endpoints: the handler owns behavior, the guard owns target authorization, and one global handler owns error responses. The host also keeps the endpoints that have no handler to sit beside: protocol endpoints, static assets, and development-only tooling, each carrying its mark.

Errors as Problem Details#

Every error response uses RFC 9457 Problem Details carrying a stable application code, the current traceId, and one errors entry per field failure. The stable code lets a client branch on the failure, and the trace identifier connects the response to its diagnostics. Error responses come from one global handler that maps each failure to its status and body.

The Generated Contract#

The OpenAPI document is generated from the code during the Release build, with no handwritten copy. It declares each operation's stable operationId, authorization, request, success, and Problem Details metadata, plus precise schemas for every field. The generated artifact is committed when a frontend or external consumer reads it, so every contract change is visible in review.

Standards#

Map One Operation per Endpoint#

ruleruleguidance
litespec/backend/api#map-one-operation-per-endpoint
Statement

An endpoint MUST map one route operation, convert input to an Application message, dispatch through its mediator, and map the result.

Applies toartifact: endpoint

Why

One endpoint class per route operation gives each route a single owner, so its input mapping, dispatch, and result mapping live in one place. An endpoint that served several operations would mix their mappings and force one class to change for unrelated routes.

Example

At startup, the application discovers endpoint implementations, registers each one once, and maps them in deterministic name order. Discovery uses the platform's own reflection and dependency injection. It needs no separate scanning package.

Checka reviewer answers

Reviewed forWhich mapped routes resolve outside one endpoint type with one mediator dispatch? Look at the route table beside the use case folders.

Marked guidance: a reviewer answers this question.

Keep Endpoint Dependencies Transport-Focused#

ruleruleguidance
litespec/backend/api#keep-endpoint-dependencies-transport-focused
Statement

An endpoint MUST NOT take an aggregate repository, a persistence session or context, a provider client, or an application service.

Applies toartifact: endpoint

Why

An endpoint that took a repository, session, provider, or application service could reach business state or providers without passing the pipeline. Endpoints hold no request-scoped dependency because the application registers them at startup; handlers take mediators and HTTP-boundary services such as an actor accessor.

Checka reviewer answers

Reviewed forWhich endpoint constructors take more than mediators and HTTP-boundary services? Look at the endpoint constructors in the use case folders.

Marked guidance: a reviewer answers this question.

Defer Error Mapping to the Global Handler#

ruleruleguidance
litespec/backend/api#defer-error-mapping-to-the-global-handler
Statement

An endpoint MUST NOT catch a use-case failure, a domain failure, or a validation failure.

Applies toartifact: endpoint

Why

One global handler produces every error response from every failure type. An endpoint that caught a failure would create a second mapping for the same failure, and the two would diverge: the same refusal would answer different statuses or codes depending on which endpoint raised it.

Checka reviewer answers

Reviewed forWhich endpoint bodies catch a use-case, domain, or validation failure? Look at the endpoint bodies beside the global handler.

Marked guidance: a reviewer answers this question.

Apply Only Coarse Policies at the Endpoint#

ruleruleguidance
litespec/backend/api#apply-only-coarse-policies-at-the-endpoint
Statement

A WebApi endpoint MUST enforce only coarse authenticated, role, or scope policies, leaving target decisions to the application guard.

Applies toartifact: endpoint

Why

Authentication establishes who is calling, not whether the caller is allowed to act on this specific resource. Coarse policies at the endpoint check the caller; the application guard decides the target from the access the use-case record declares. An endpoint that decided target authorization would duplicate business data access the guard already owns. Target authorization lives in litespec/backend/application#enforce-target-authorization-in-the-pipeline.

An operation applies a stable forbidden or not-found policy when revealing that a resource exists would leak information.

Checka reviewer answers

Reviewed forWhich endpoint policies decide target authorization instead of coarse authentication, role, or scope? Look at the endpoint policies beside the application guard.

Marked guidance: a reviewer answers this question.

Return Stable Problem Details#

ruleruleguidance
litespec/backend/api#return-stable-problem-details
Statement

An error response MUST use RFC 9457 Problem Details carrying a stable application code, the current traceId, and an errors entry for each field failure.

Applies toartifact: any

Why

A stable machine-readable code lets a client branch on the failure without parsing prose, and the trace identifier connects the response to its diagnostics. One entry per field failure tells the caller everything wrong with the request in one answer. RFC 9457 defines the Problem Details media type the contract uses.

Example

The serialized contract is:

JSON
{
  "type": "https://example.test/problems/validation",
  "title": "Request validation failed",
  "status": 400,
  "detail": "One or more values are invalid.",
  "instance": "/api/posts",
  "code": "validation-failed",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
  "errors": [
    {
      "field": "title",
      "code": "required",
      "message": "Title is required."
    }
  ]
}

type is a stable absolute URI owned by the consumer. errors appears only when validity findings exist, carrying their member, code, and message. code carries the refusal result's stable code, and detail carries the free-form prose explanation. A request body names the same pair code and reason under litespec/backend/api#name-failure-pairs-code-and-reason.

WebApi maps Application member names to their public JSON field names. traceId carries the trace identifier from the current W3C Trace Context traceparent, with the request identifier as fallback. The same shape applies to authentication and authorization failures.

The global handler maps validation failures to 400, missing targets through the operation's disclosure policy, and forbidden failures to 403 or the declared 404. The derived invalid-input refusal answers 400, and the derived forbidden refusal answers 403 or the declared 404. It maps conflicts and state rejections to 409, and unexpected failures to 500 with code internal-error. It maps each known domain failure individually. It does not report cancellation from a disconnected request as an application error.

In the Claimwick product, reimbursing an expense that is not approved ends with result/reimburse-expense.not-approved, which carries the code spending.not-approved and the status 409. For example, the global handler answers:

JSON
{
  "type": "https://example.test/problems/conflict",
  "title": "Not approved",
  "status": 409,
  "detail": "The expense is not approved.",
  "instance": "/api/expenses",
  "code": "spending.not-approved",
  "traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}

The endpoint catches no failure. The diagram shows how a domain failure or a validation failure reaches the global handler and becomes a status with a Problem Details body.

How a failure becomes an HTTP error response A domain exception or a validation failure reaches the global handler, which no endpoint preempts. The handler selects the HTTP status and returns a Problem Details body carrying the stable code, the trace identifier, and one entry per field failure. Domain exception Validation failure Global handler HTTP status and Problem Details business rule rejected structural input rejected The body carries code, traceId, and one entry per field failure. An arrow points from a failure to the stage that handles it.
A domain failure or a validation failure reaches the global handler, which no endpoint preempts. The handler selects the HTTP status and returns a Problem Details body carrying the stable code, the trace identifier, and one entry per field failure.
Checka reviewer answers

Reviewed forWhich error responses miss the stable code, the traceId, or one errors entry per field failure? Look at the error responses at the API boundary.

Marked guidance: a reviewer answers this question.

Use Consistent Status Codes#

ruleruleguidance
litespec/backend/api#use-consistent-status-codes
Statement

A WebApi operation MUST return the status the outcome table assigns to its result.

Applies toartifact: endpoint

Why

A client branches on status before it reads a body, so a status that disagrees with the outcome table hides the real outcome. The stack derives the status from the use-case result: each success and refusal kind maps to exactly one status, with no recorded status on the record. The outcome table assigns each result its status once, and the operation returns exactly that status.

Example

OutcomeStatus
Successful read200
Successful creation of a readable resource201 with the resource location
Successful creation without a read route201
Successful command without a response body204
Structural validation failure400
Missing or invalid authentication401
Authenticated but forbidden403
Resource unavailable under the disclosure policy404
State, version, or idempotency conflict409
Abuse limit exceeded429 with a Retry-After header
Accepted background operation202 with status location
Unexpected failure500
Checka reviewer answers

Reviewed forWhich operation responses return a status outside the outcome table? Look at each response beside the outcome table.

Marked guidance: a reviewer answers this question.

Bound Every File Upload#

ruleruleguidance
litespec/backend/api#bound-every-file-upload
Statement

A file upload operation MUST accept no file larger than the size its use-case record declares and no media type outside the media types its use-case record declares.

Applies toartifact: endpoint

Why

An upload without a declared size or media-type bound lets one caller send work the server never agreed to store or inspect. Declaring both bounds on the use-case record lets the server refuse an oversize file or an undeclared type before storage, and lets a reviewer see the limit beside the use case that owns the file.

Example

The Claimwick submit-expense upload declares the size and the image media types a receipt photo may carry, and the server refuses a larger file or another media type before it stores anything.

Checka reviewer answers

Reviewed forWhich upload operations lack a declared size or media-type bound, or accept a file outside either bound? Look at each upload endpoint beside its use-case record.

Marked guidance: a reviewer answers this question.

Prove Every Upload Type from Its Content#

ruleruleguidance
litespec/backend/api#prove-every-upload-type-from-its-content
Statement

A file upload operation MUST determine the file type from the file content rather than the media type or file name the client claims.

Applies toartifact: endpoint

Why

A claimed media type and a file name are caller input, so either one can name an image while the bytes carry anything else. Reading the type from the content itself keeps the declared media-type bound honest, because the check runs on what the server stores rather than on what the caller says it sent.

Example

A Claimwick receipt photo named as an image is stored as an image only when its bytes read as one of the declared image types, and bytes of another type are refused even when the name and claimed type agree.

Checka reviewer answers

Reviewed forWhich upload operations trust a claimed media type or file name over the file bytes? Look at the type check of each upload endpoint beside the declared media types.

Marked guidance: a reviewer answers this question.

Store Every Upload Through Infrastructure#

ruleruleguidance
litespec/backend/api#store-every-upload-through-infrastructure
Statement

File storage for an upload MUST live in Infrastructure.

Applies toartifact: command-handler, endpoint

Why

Storing a file is provider work, like storing an aggregate, so the endpoint stays a transport adapter and the handler stays on behavior. Infrastructure owns the storage provider, its configuration, and its errors, so a project can change where files live without touching an endpoint or a handler.

Example

The Claimwick endpoint for a receipt photo converts the request to its command and dispatches it, the handler stages the expense, and Infrastructure writes the photo bytes to the configured store.

Checka reviewer answers

Reviewed forWhich endpoints or handlers write file bytes to a storage provider directly? Look at the upload endpoints and handlers beside the Infrastructure storage code.

Marked guidance: a reviewer answers this question.

Authorize Every File Download Through Its Owning Use Case#

ruleruleguidance
litespec/backend/api#authorize-every-file-download-through-its-owning-use-case
Statement

A caller MUST reach a stored file only through a download use case authorized from the access that use case declares.

Applies toartifact: endpoint

Why

A stored file carries the data of the record it belongs to, so a direct storage address would serve that data to anyone holding the address and skip the owning use case's grant and tenant scope. Routing each download through its owning use case keeps one authorization decision for the file and for the record it belongs to.

Example

A Claimwick receipt photo downloads through the use case that owns the expense, so only an actor its access allows reads the photo, and no storage address serves it on its own.

Checka reviewer answers

Reviewed forWhich stored files are reachable outside a download use case and its declared access? Look at the download endpoints and the storage addresses beside each owning use case.

Marked guidance: a reviewer answers this question.

ruleruleguidance
litespec/backend/api#require-the-marker-header-on-cookie-authenticated-writes
Statement

The API MUST refuse a cookie-authenticated write without the project's marker header.

Applies toartifact: any

Why

Cookies travel with every request, including ones a malicious page triggers, so a cookie-authenticated write needs proof the project's own page sent it. A page on another site adds no custom header, so the header's absence exposes a cross-site forgery. A write is any request other than GET, HEAD, or OPTIONS. The header takes the form X-<Project>-Request: 1, with the exact name set in project configuration.

The refused request is unauthenticated, so it answers 401 under litespec/backend/api#use-consistent-status-codes. The frontend pages own sending the header. This rule owns the API refusal.

Checka reviewer answers

Reviewed forWhich cookie-authenticated writes without the marker header escape the 401 answer? Look at the write requests with cookie authentication.

Marked guidance: a reviewer answers this question.

Keep Routes Resource-Oriented#

ruleruleguidance
litespec/backend/api#keep-routes-resource-oriented
Statement

A route MUST use lowercase plural resource segments, kebab-case subresources, route parameters for identity, query parameters for filtering, and the body for command data.

Applies toartifact: endpoint

Why

A predictable route shape lets a caller derive an unfamiliar operation from a familiar one: plural resource segments, kebab-case subresources, route parameters for identity, query parameters for filtering, and command data in the body. A route that encoded actions or identities anywhere else would force the caller to read each route's documentation. For a transition beyond the five verbs, AIP-136 defines the custom-method form.

Example

A route carries the authenticated actor identifier only when that actor is intentionally addressing another actor as a resource.

Checka reviewer answers

Reviewed forWhich routes break the plural segments, kebab-case subresources, identity parameters, filter queries, or body command data? Look at the route table entries.

Marked guidance: a reviewer answers this question.

Apply PATCH to a Partial Update#

ruleruleguidance
litespec/backend/api#apply-patch-to-a-partial-update
Statement

A WebApi operation MUST apply a partial update with PATCH and the application/merge-patch+json media type.

Applies toartifact: endpoint

Why

A partial update names only the fields it changes. Sending them with PATCH and the merge-patch media type gives those semantics a standard meaning; a PUT body for a partial change either repeats fields the caller does not change or invents merge semantics per field. RFC 5789 defines PATCH and RFC 7386 defines the merge-patch media type.

Example

A partial update sends only the changed members with Content-Type: application/merge-patch+json, and the endpoint reads the body as a sparse patch. A PUT keeps replacing the whole resource (RFC 9110 section 9.3.4), and DELETE stays a hard-state transition.

Checka reviewer answers

Reviewed forWhich partial updates arrive outside PATCH with the merge-patch media type? Look at the partial update requests.

Marked guidance: a reviewer answers this question.

Bound Collection Queries#

ruleruleguidance
litespec/backend/api#bound-collection-queries
Statement

A collection endpoint MUST apply deterministic ordering, its record's declared default limit, and its record's declared maximum limit of 100 or lower.

Applies toartifact: endpoint

Why

An unbounded collection read turns one caller into a source of unbounded database and serialization work. Deterministic ordering keeps pages stable, the declared default limit bounds the common case, and the declared maximum of 100 or lower caps the worst case. The use-case record declares its paging with order, cursor or offset, default limit, and maximum limit, and the endpoint applies exactly that. Cursor mechanics live in litespec/backend/api#use-opaque-cursors-for-collection-paging and litespec/backend/api#version-cursors-with-stable-sort-values.

Example

A request uses after as an opaque cursor and limit as a positive integer. The implementation fetches one record beyond the requested limit to determine whether another page exists. The response uses one shape:

JSON
{
  "items": [],
  "page": {
    "limit": 20,
    "nextCursor": null,
    "hasMore": false
  }
}
Checka reviewer answers

Reviewed forWhich collection responses lack deterministic order, or exceed the declared default or maximum limit? Look at the collection responses beside their records.

Marked guidance: a reviewer answers this question.

Use Opaque Cursors for Collection Paging#

ruleruleguidance
litespec/backend/api#use-opaque-cursors-for-collection-paging
Statement

A collection endpoint MUST page with an opaque cursor and refuse a malformed cursor with 400 and code invalid-cursor.

Applies toartifact: endpoint

Why

A cursor that exposed offsets or internals lets callers craft requests against storage details and breaks when those details change. An opaque cursor keeps paging state server-defined, and the endpoint treats it as untrusted input: a malformed cursor, or an unsupported cursor version, answers 400 with code invalid-cursor.

Checka reviewer answers

Reviewed forWhich malformed cursors escape the 400 answer with code invalid-cursor? Look at the collection endpoints.

Marked guidance: a reviewer answers this question.

Version Cursors with Stable Sort Values#

ruleruleguidance
litespec/backend/api#version-cursors-with-stable-sort-values
Statement

A paging cursor MUST carry a version and the last stable sort values, including a unique tie-breaker, and no sensitive value in readable form.

Applies toartifact: endpoint

Why

Paging walks a sorted sequence, so the cursor names exactly where the last page stopped: the version lets the sort evolve without breaking stored cursors, and the last stable sort values with a unique tie-breaker keep the walk exact when values repeat. The tie-breaker matches the query's stable order under litespec/backend/api#bound-collection-queries. No sensitive value travels in readable form, because callers store and replay cursors.

Checka reviewer answers

Reviewed forWhich cursor payloads miss a version, the last sort values, or a unique tie-breaker, or leak a readable secret? Look at the cursor payloads.

Marked guidance: a reviewer answers this question.

Treat OpenAPI as a Generated Contract#

ruleruleguidance
litespec/backend/api#treat-openapi-as-a-generated-contract
Statement

An application MUST generate its OpenAPI document during the Release build.

Applies toartifact: any

Why

A handwritten contract drifts from the code it describes with the first unmirrored change. Generating the document during the Release build keeps the contract a function of the code, so the file the pinned generator reads stays current.

Example

The API source artifact is apps/api/openapi/{ProjectName}.json.

Build-time generation boots the host. A boot reaches every startup guard. A project whose composition refuses to start without a secret, a certificate, or a reachable dependency generates no contract from a clean checkout. Two answers are valid, and the project records which one it uses under litespec/backend/api#generate-the-contract-without-hosted-dependencies.

AnswerWhat the project doesWhat it costs
Generation environmentSet an environment for the generation run whose composition wires no hosted dependency.The generated document reflects that environment's endpoint set.
Deferred guardMove each startup refusal from composition to first use.A misconfiguration surfaces on the first request instead of at boot.

Generation without hosted dependencies follows litespec/backend/api#generate-the-contract-without-hosted-dependencies.

When a typed consumer reads the API, the pinned generator reads the source artifact. The generated client, its model types, its request schemas, and its query options are one workspace package, because every frontend and every command reads them.

Checka reviewer answers

Reviewed forWhich Release builds skip regenerating the OpenAPI artifact, or which contract gates pass on a changed tree? Look at the Release build output and the contract gate.

Marked guidance: a reviewer answers this question.

Declare Stable Operation Metadata in the Contract#

ruleruleguidance
litespec/backend/api#declare-stable-operation-metadata-in-the-contract
Statement

An operation MUST declare a stable operationId, its authorization, request, success, and Problem Details metadata, and a summary.

Applies toartifact: endpoint

Why

Generated clients name their methods from operationId, so an unstable name renames client code on every generation. Authorization metadata cites the enforced policies and Problem Details metadata covers every documented failure status, so the client models security and failure before its first call. Comments on handlers are not contract documentation, so operations carry explicit summaries and descriptions.

Checka reviewer answers

Reviewed forWhich operations in the generated contract miss operationId, authorization, request, success, failure, or summary entries? Look at the operations in the generated contract.

Marked guidance: a reviewer answers this question.

Name the Use Case on Every Published Operation#

rulerulecheckable
litespec/backend/api#name-the-use-case-on-every-published-operation
Statement

Every operation in the generated contract MUST carry x-litespec-use-case naming the use case its endpoint implements.

Applies toartifact: endpoint

Why

The operationId names an operation for client code and keeps its casing for method names, so it is not the record identifier. Carrying the identifier itself in an extension, which the OpenAPI specification reserves for this kind of metadata, lets an integrator, a gateway log, or a support person go from an operation straight to its specification page. The value comes from the generated map of operation names to use cases, so it is written once, in the endpoint's mark.

Example

The Claimwick operation SubmitExpense carries "x-litespec-use-case": "use-case/spending.submit-expense".

Checkchecked by a tool

Checked bylitespec model check reports operation-use-case-drift (level S) for an operation in the committed contract whose x-litespec-use-case is missing or differs from the use case its endpoint's mark names.

Marked checkable: the tool named here checks it.

Commit the Generated Contract Its Consumers Read#

ruleruleguidance
litespec/backend/api#commit-the-generated-contract-its-consumers-read
Statement

An application MUST commit its generated OpenAPI artifact when a frontend or external consumer reads it.

Applies toartifact: any

Why

A committed artifact makes every contract change visible in review and lets CI fail when the document was not regenerated. Without the committed file, a breaking contract change hides inside a code change. Consumers read the artifact through the generated client, model, schema, and query-option packages.

Checka reviewer answers

Reviewed forWhich frontends or external consumers read an uncommitted or stale OpenAPI artifact? Look at the committed artifact beside its consumers.

Marked guidance: a reviewer answers this question.

Reflect Enforced Authentication in the Contract#

ruleruleguidance
litespec/backend/api#reflect-enforced-authentication-in-the-contract
Statement

A generated OpenAPI document MUST declare the security scheme and per-operation security requirement for every endpoint that enforces authentication.

Applies toartifact: any

Why

A consumer that learns an authentication requirement from a runtime 401 has already built the wrong client: declared security schemes and per-operation requirements let it build the right one. Handwritten security metadata drifts from registered schemes, so the document derives them from the registered authentication schemes.

Example

The implementation reads the registered authentication schemes and adds the matching securitySchemes and security entries. Handwritten security metadata drifts from registered schemes. An operation whose access grant is anonymous declares no security requirement.

Checka reviewer answers

Reviewed forWhich authenticated operations miss their security schemes and requirements in the generated contract? Look at the operations in the generated contract.

Marked guidance: a reviewer answers this question.

Publish Precise, Complete Schemas#

ruleruleguidance
litespec/backend/api#publish-precise-complete-schemas
Statement

A generated schema MUST declare the closed value set, bounds, format, and required control headers of each field and parameter it describes.

Applies toartifact: endpoint

Why

A caller that learns limits from the document validates before sending; a caller that learns them from runtime rejections discovers one limit per failed request. Declaring the closed value set, bounds, format, and required control headers of each field and parameter moves every checkable limit into the contract.

Example

A label-only set publishes its values as an OpenAPI enum. A data-bearing set publishes a polymorphic oneOf model with a discriminator per litespec/backend/api#mirror-a-domain-closed-set-as-a-transport-model-of-the-same-shape. A converter on the boundary type keeps every serializer on the contract, including a test client using default options. Host-only conversion breaks readers that do not share host configuration.

An operation requiring a control header declares it as a required parameter, for example Idempotency-Key or If-Match. The implementation prefers results, enums, and parameter metadata derived from code over handwritten schemas that drift from code.

Checka reviewer answers

Reviewed forWhich contract schemas miss value sets, bounds, formats, or required control headers for a field? Look at the schemas in the generated contract.

Marked guidance: a reviewer answers this question.

Expose Development Tooling in Development Only#

ruleruleguidance
litespec/backend/api#expose-development-tooling-in-development-only
Statement

An application MUST map its API exploration and diagnostic endpoints only when the host runs in the Development environment.

Applies toartifact: any

Why

Development tooling exposes the full API surface for exploration, which is what development needs and what a hosted environment leaves unoffered. Mapping it in Development only keeps exploration local while hosted environments serve only the API.

Checka reviewer answers

Reviewed forWhich hosted environments serve exploration tooling, or which Development hosts hide it? Look at the mapped endpoints in each environment.

Marked guidance: a reviewer answers this question.

Mirror a Domain Closed Set as a Transport Model of the Same Shape#

ruleruleguidance
litespec/backend/api#mirror-a-domain-closed-set-as-a-transport-model-of-the-same-shape
Statement

A WebApi transport model for a Domain closed set MUST preserve the shape of that set.

Applies toartifact: endpoint

Why

Each case of a closed set carries different data, and a transport shape that differs from its Domain set discards information the Domain carries. Preserving the shape keeps every case and its data on the wire. Label-only sets follow litespec/backend/api#publish-label-only-sets-as-string-enums, and data-bearing sets follow litespec/backend/api#mark-polymorphic-models-with-discriminators. Narrowing follows litespec/backend/application#record-a-decision-for-closed-set-narrowing.

Example

A label-only set maps to a string enum. A data-bearing set maps to a polymorphic model with one case per variant, published as oneOf with a discriminator.

Checka reviewer answers

Reviewed forWhich transport models drop a case or its data from their Domain set? Look at each transport model beside its Domain set.

Marked guidance: a reviewer answers this question.

Publish Label-Only Sets as String Enums#

ruleruleguidance
litespec/backend/api#publish-label-only-sets-as-string-enums
Statement

A transport model for a label-only set MUST publish its values as an OpenAPI string enum.

Applies toartifact: endpoint

Why

A label-only set carries no case data, so its labels are the complete contract and a string enum carries them exactly. The permission to use a plain enum for this one situation lives in litespec/backend/domain#model-every-closed-set-of-domain-values-as-a-closed-type-hierarchy.

Checka reviewer answers

Reviewed forWhich label-only sets escape the string enum in the contract? Look at the label-only sets in the contract.

Marked guidance: a reviewer answers this question.

Mark Polymorphic Models with Discriminators#

ruleruleguidance
litespec/backend/api#mark-polymorphic-models-with-discriminators
Statement

A data-bearing set MUST map to a polymorphic model with one case per variant, published as oneOf with a string discriminator.

Applies toartifact: endpoint

Why

A polymorphic payload without a discriminator forces the reader to guess the case from its fields. A required string discriminator names the case on every payload, so no payload validates without one. The strings equal the Domain union's stable case codes and stay stable across identifier renames.

Example

  • Declare one polymorphic base with one registered case per variant.
  • Carry the metadata on the type, not only in host options.
  • Name the discriminator property, using a name such as type or outcome.
  • Opt cases in explicitly, so an unregistered subtype fails serialization.
  • A document or schema transformer completes discriminator metadata that the generator omits.
Checka reviewer answers

Reviewed forWhich data-bearing sets escape the oneOf model with a required string discriminator? Look at the data-bearing sets in the contract.

Marked guidance: a reviewer answers this question.

Reject a Collapsed or Borrowed Wire Contract#

ruleruleguidance
litespec/backend/api#reject-a-collapsed-or-borrowed-wire-contract
Statement

A WebApi transport model MUST NOT collapse a data-bearing set into an enum or reuse a Domain or Application type as the wire contract.

Applies toartifact: endpoint

Why

Collapsing a data-bearing set into an enum discards the data that distinguishes its cases, and reusing a Domain or Application type as the wire contract couples the public contract to inner shapes. Each layer owns its own contract type per litespec/backend/architecture#own-each-layers-contract-types, so the wire contract is always a dedicated transport model. Narrowing follows litespec/backend/application#record-a-decision-for-closed-set-narrowing.

Checka reviewer answers

Reviewed forWhich wire contracts collapse a data-bearing set into an enum or reuse a Domain or Application type? Look at each wire contract.

Marked guidance: a reviewer answers this question.

Keep One Name for a Use Case Across Layers#

ruleruleguidance
litespec/backend/api#keep-one-name-for-a-use-case-across-layers
Statement

Each use case MUST carry its name unchanged in its message type, operation id, and audit action.

Applies toartifact: endpoint, use-case

Why

A use case that reads one way in its record, another in its message type, and a third in its audit action forces every trace across layers through a translation table only the author holds. The record id is the name; the message type, the endpoint operation id, and the audit action repeat it in their own casing, so a reader follows one name from the page to the wire to the audit log.

Example

use-case/spending.reimburse-expense travels as ReimburseExpenseCommand, operation id ReimburseExpense, and audit action reimburse-expense; an operation id of PayExpense is reported.

Checka reviewer answers

Reviewed forWhich message types, operation ids, or audit actions carry a different name than their use case? Look at each use case's message type, operation id, and audit action.

Marked guidance: a reviewer answers this question.

Conventions#

Use This Endpoint Layout#

conventiondefaultguidance
litespec/backend/api#use-this-endpoint-layout
Default

Place each endpoint with its request and response models in its use case folder, shared transport types in the node WebApi/ folders, and host-only endpoints in the host.

ReplacementA project names its endpoint layout in its backend documentation.

Applies toartifact: endpoint

Why

An endpoint in its use case folder makes the folder path match the specification path. A reader locates an operation from its use-case identifier without searching. Transport types used by use cases of two or more aggregates sit in the module or aggregate WebApi/ folder, while the host keeps the endpoints that have no handler to sit beside: protocol endpoints, static assets, and development-only tooling.

Example

The endpoints sit in their use case folders inside the UseCases/ folder of their module and aggregate:

Text
{Product}/
  Posts/                            single aggregate whose name matches the module
    WebApi/                         transport types used by two or more Posts use cases
    UseCases/
      CreateDraft/                  endpoint, request model, and response model beside the handler
      GetPost/
  Customers/                        two aggregates: operations nest under each aggregate UseCases folder
    CustomerAccounts/
      WebApi/                       transport types used by two or more CustomerAccounts use cases
      UseCases/
        RestrictAccount/
    Consents/
      UseCases/
        GrantConsent/

Module nesting, including the single-aggregate flat form, follows litespec/backend/architecture#organize-code-by-module-aggregate-and-use-case.

Checka reviewer answers

Reviewed forWhich endpoints sit outside their use case folder or the host, or which shared transport types sit outside the node WebApi/ folders? Look at the endpoints beside the use case folders.

Marked guidance: a reviewer answers this question.

Name Transport Types by Their Boundary Role#

conventiondefaultguidance
litespec/backend/api#name-transport-types-by-their-boundary-role
Default

Transport types take the names the suffix table assigns their roles.

ReplacementA project names its transport suffixes in its backend documentation.

Applies toartifact: endpoint

Why

A name that ends in its boundary role keeps transport concerns visibly separate from Application messages and results. The suffix table that assigns each role its suffix lives in litespec/workspace/naming#use-architectural-suffixes.

Example

A passive HTTP DTO names its boundary role and ends in Model.

Checka reviewer answers

Reviewed forWhich transport type names miss the suffix their boundary role assigns? Look at the transport type names.

Marked guidance: a reviewer answers this question.

Name Routes from Resources#

conventiondefaultguidance
litespec/backend/api#name-routes-from-resources
Default

Derive each route from its resource and subresource, and use an action segment only when the operation maps to neither.

ReplacementA project names its route shape in its backend documentation.

Applies toartifact: endpoint

Why

A route derived from its resource reads the same for every operation: the caller knows the record before opening the handler. An action segment reserved for operations that map to neither resource nor subresource keeps the exception visible instead of spreading verb-led routes.

Example

Text
POST   /api/posts
GET    /api/posts/{postId}
GET    /api/posts/{postId}/publication
GET    /api/posts?after={cursor}&limit=20
Checka reviewer answers

Reviewed forWhich routes derive outside their resource and subresource, or carry an action segment where either fits? Look at the route table.

Marked guidance: a reviewer answers this question.

Shape Command Routes from the Aggregate and the Verb#

conventiondefaultguidance
litespec/backend/api#shape-command-routes-from-the-aggregate-and-the-verb
Default

A command route carries the aggregate plural, and a custom action ends with the use-case verb as its last segment; a POST to the collection, a PUT, PATCH or DELETE on one resource, and a verb the HTTP method already says (POST for create, add and register; PUT or PATCH for change; DELETE for remove) keep the plain resource route.

ReplacementA project names its route shape in its backend documentation.

Applies toartifact: endpoint

Why

A route that names the aggregate plural, its identity, and the verb as the last segment reads the same for every command: the reader knows the record and the act before opening the handler. A route that repeats the noun or hides the verb makes each command route a fresh puzzle.

Example

Text
POST   /api/expenses/{id}/reimburse
POST   /api/holds/{holdId}/cancel
Checka reviewer answers

Reviewed forWhich command routes end outside their verb or carry no aggregate plural? Look at the command routes.

Marked guidance: a reviewer answers this question.

Keep Numeric Transport Types Precise#

conventiondefaultguidance
litespec/backend/api#keep-numeric-transport-types-precise
Default

Emit a plain numeric schema for an int32 or double field, and reserve a string-or-number union for int64 values beyond the JSON safe integer range.

ReplacementA project names its numeric wire shapes in its backend documentation.

Applies toartifact: endpoint

Why

A plain numeric schema lets consumers read int32 and double directly. A string-or-number union is reserved for int64 values beyond the JSON safe integer range, because an over-broad union forces every consumer to coerce plain numbers.

Checka reviewer answers

Reviewed forWhich numeric fields publish outside plain schemas, or reserve the int64 union wrongly? Look at the numeric fields in the contract.

Marked guidance: a reviewer answers this question.

Model a Monetary Value as One Object#

conventiondefaultguidance
litespec/backend/api#model-a-monetary-value-as-one-object
Default

Carry a monetary value as one Money object in a response model and in a request where the caller chooses the currency.

ReplacementA project names its money wire shape in its backend documentation.

Applies toartifact: endpoint

Why

Split sibling fields pair only by a server invariant invisible to the schema, so a report reads the two independently and mismatches them. One Money object holding amount and an ISO 4217 currency mirrors the Domain shape on the wire under litespec/backend/domain#model-money-with-exact-decimal-values. A caller with no currency choice carries amount alone, because a fixed currency arrives from the account, not the caller.

Example

unitPrice and currencyCode become price of type Money. A request that references an account with a fixed currency carries amount alone.

Checka reviewer answers

Reviewed forWhich monetary values travel outside one Money object, or carry currency where the caller has no choice? Look at the monetary fields in the contract.

Marked guidance: a reviewer answers this question.

Name Boolean Fields as Conditions#

conventiondefaultguidance
litespec/backend/api#name-boolean-fields-as-conditions
Default

Boolean fields use the same condition names as code, in camel case.

ReplacementA project names its boolean wire shape in its backend documentation.

Applies toartifact: endpoint

Why

Condition names state what the boolean means, and the camel-case form matches wire conventions. Boolean names live in litespec/workspace/naming#use-intent-revealing-boolean-names, and transport carries the camel-case form.

Example

IsBuyable in code becomes isBuyable on the wire.

Checka reviewer answers

Reviewed forWhich boolean fields break the camel-case condition names of their code names? Look at the boolean fields in the contract.

Marked guidance: a reviewer answers this question.

Name Time Fields with At#

conventiondefaultguidance
litespec/backend/api#name-time-fields-with-at
Default

Time fields end in At, following AIP-142.

ReplacementA project names its time field shape in its backend documentation.

Applies toartifact: endpoint

Why

One At suffix tells the reader every time field carries a timestamp. A mixed suffix set hides which shape each field carries.

Example

createdAt names the creation time.

Checka reviewer answers

Reviewed forWhich time fields in contract schemas end outside At? Look at the time fields.

Marked guidance: a reviewer answers this question.

Use Standard Field Vocabulary#

conventiondefaultguidance
litespec/backend/api#use-standard-field-vocabulary
Default

Field names use created over placed, languageTag over locale, and displayName over keyName.

ReplacementA project names its field vocabulary in its backend documentation.

Applies toartifact: endpoint

Why

Shared vocabulary stops the same field from appearing under different names on different operations. languageTag follows RFC 5646.

Example

defaultLanguageTag replaces defaultLocaleTag.

Checka reviewer answers

Reviewed forWhich fields use placed, locale, or keyName over the standard vocabulary? Look at the field names in the contract.

Marked guidance: a reviewer answers this question.

Name Failure Pairs Code and Reason#

conventiondefaultguidance
litespec/backend/api#name-failure-pairs-code-and-reason
Default

A request body names a failure with code for the class and reason for the explanation.

ReplacementA project names its failure pair shape in its backend documentation.

Applies toartifact: endpoint

Why

A request body names failures the same way responses do: code for the class, reason for the explanation, mirroring the Problem Details code and detail under litespec/backend/api#return-stable-problem-details. One pair in both directions lets clients reuse their failure handling.

Checka reviewer answers

Reviewed forWhich request bodies name failures outside code and reason? Look at the failure pairs in request bodies.

Marked guidance: a reviewer answers this question.

Name Path Parameters per Resource#

conventiondefaultguidance
litespec/backend/api#name-path-parameters-per-resource
Default

Each path parameter names its own resource identity, and a details segment takes the aggregate name.

ReplacementA project names its path parameter shape in its backend documentation.

Applies toartifact: endpoint

Why

Each path parameter names its own resource identity, so /api/posts/{postId} is mistaken for no other resource's key. A shared id hides which resource it identifies.

Example

/api/posts/{postId} names the post; /details takes the aggregate name.

Checka reviewer answers

Reviewed forWhich path parameters miss their own resource identity? Look at the path parameters.

Marked guidance: a reviewer answers this question.

Avoid Noise Words in Field Names#

conventiondefaultguidance
litespec/backend/api#avoid-noise-words-in-field-names
Default

Field names carry no type prefix and no noise word.

ReplacementA project names its field name rules in its backend documentation.

Applies toartifact: endpoint

Why

A type prefix repeats what the schema already declares, and a noise word names nothing an integrator acts on: customerInfo carries no more meaning than customer. The noise words are data, info, item, object, value, and thing; the type prefixes are str, int, bool, num, obj, and arr.

Example

customerInfo becomes customer.

Checka reviewer answers

Reviewed forWhich field names in contract schemas carry a type prefix or noise word? Look at the field names.

Marked guidance: a reviewer answers this question.

Declare a Smaller Collection Maximum#

conventiondefaultguidance
litespec/backend/api#declare-a-smaller-collection-maximum
Default

A use case with stricter needs declares its smaller maximum in its record's paging and states the reason on its use-case record.

ReplacementA project names its collection limit shape in its backend documentation.

Applies toartifact: record:use-case

Why

Some collections cost more per item than others. A use case with stricter needs declares its smaller maximum in its record's paging with the reason on its use-case record, and the endpoint enforces it under litespec/backend/api#bound-collection-queries.

Checka reviewer answers

Reviewed forWhich lowered collection maxima miss their value and reason on the use-case record? Look at each lowered maximum.

Marked guidance: a reviewer answers this question.

Use Offsets for Proven Bounded Datasets#

conventiondefaultguidance
litespec/backend/api#use-offsets-for-proven-bounded-datasets
Default

A collection over a proven bounded dataset declares offset paging in its record while the public contract stays cursor-shaped.

ReplacementA project names its paging shape in its backend documentation.

Applies toartifact: record:use-case

Why

Offsets skip rows the database still scans, so they stay cheap only while the dataset stays small. A collection over a proven bounded dataset declares offset paging while the public contract stays cursor-shaped, and the use-case record states the bound that makes it safe.

Checka reviewer answers

Reviewed forWhich offset paging lacks the bound making it safe on the use-case record? Look at each offset-paged collection.

Marked guidance: a reviewer answers this question.

Generate the Contract Without Hosted Dependencies#

conventiondefaultguidance
litespec/backend/api#generate-the-contract-without-hosted-dependencies
Default

The generation run contacts no hosted dependency and runs no schema change, and the project records its chosen answer.

ReplacementA project names its generation setup in its backend documentation.

Applies toartifact: any

Why

Build-time generation boots the host, and a boot that needs a secret, a certificate, or a reachable dependency fails on a clean checkout. The project records which answer it uses: a generation environment that wires no hosted dependency for the generation run, or a deferred guard that moves each startup refusal from composition to first use. The document reflects the generation environment's endpoint set, and with a deferred guard a misconfiguration surfaces on the first request instead of at boot.

Checka reviewer answers

Reviewed forWhich generation runs contact a hosted dependency, or lack a recorded answer? Look at the generation runs beside the project record.

Marked guidance: a reviewer answers this question.

Reference Example#

This informative example demonstrates litespec/backend/api#keep-endpoint-dependencies-transport-focused and litespec/backend/api#mirror-a-domain-closed-set-as-a-transport-model-of-the-same-shape.

A create endpoint reads the author from claims. It maps the request model to the command through the endpoint mappings. It sends the command through the command mediator. It maps the result to the response model with the new resource location. A read endpoint maps the query result through its own mappings. No endpoint calls the aggregate factory or the Domain repository.

A refund outcome is a closed set whose cases carry different data. Domain models it once. Application mirrors it per litespec/backend/application#mirror-a-domain-closed-set-in-the-result, and WebApi mirrors it again as a polymorphic transport model rather than serializing either inner type.