effx
Authoring

Http.Contract

Request channels, response shape, middleware and OpenAPI metadata for one HTTP operation.

Http.Contract states the request and response shape of one HTTP operation. It contributes an HttpContract extension node, linked to its operation by ExtensionOf. It attaches to an operation that has an HTTP verb annotation. It is explicit on every HTTP operation, even when a group supplies root and group.

// decorator
@Http.Contract({ group: "users", params: Params, success: User.Public, metadata: { operationId: "users.get" } })

// builder
.http.contract({ group: "users", params: Params, success: User.Public, metadata: { operationId: "users.get" } })

Spec 0005 defines the contract; spec 0013 lets a group fill root, group, success, middleware and metadata.annotator (HTTP).

HttpContractOptions

FieldTypeOptionalMeaning
rootstringyesRoot API identifier. Default "effx". Each distinct root emits a separate HttpApi.
groupstringyesHttpApiGroup.make identifier. Required in the IR: omit it only when a group association supplies it.
paramsSchema.TopyesPath parameters. Its fields must match the route's :param names exactly; present if and only if the path has parameters.
querySchema.Top or trueyesQuery channel. Emitted as the Effect v4 endpoint query option. true means the operation's input; only a GET Query in a group (see below).
headersSchema.TopyesRequest header schema.
payloadSchema.TopyesRequest body schema. GET cannot declare one.
payloadIsQuerybooleanyesMarks a POST body as read-only query input. Default false. See below.
successSchema.TopyesSuccess response schema, independent of the operation's domain result. Required in the IR; a group association defaults it to the operation's success.
statusnumberyesSuccess status. The IR requires an integer from 100 to 599.
mediaTypestringyesRequest media type. Requires an explicit payload.
responseHeadersSchema.TopyesResponse header schema. The handler must return the typed header-bearing response.
conditionalbooleanyesDefault false. GET only: adds a bodyless 304 beside the header-bearing success. Requires responseHeaders. The handler owns ETag logic.
middlewareReadonlyArray<ServiceLike>yesHttpApiMiddleware.Service marker classes, attached in declaration order. effx never instantiates them.
metadataobject, table belowyesOpenAPI fields and application hooks.

Source: packages/runtime/src/Annotation.ts (HttpContractOptions); IR defaults and requirements in packages/compiler/src/extensions/http-contract.ts; semantics in docs/specs/0005-http-contract-extension.md §"Source syntax and lowering".

metadata

FieldTypeOptionalMeaning
annotatorHttpOperationAnnotatoryesExported application function. Gets HttpOperationMetadata, returns a Context merged onto the endpoint after the built-in OpenApi.annotations. Lowered to a SymbolRef, never evaluated.
operationIdstringyesOpenAPI operation ID. For a named endpoint it must be <group>.<endpointKey>; the key names the endpoint, raw binding, guard and client lookup.
commandIdentityExportedFunctionSymbolyesExported function for a typed client helper. Not sent to OpenAPI. Command only; headers must include idempotency-key and if-match.
summarystringyesOpenAPI summary.
descriptionstringyesOpenAPI description.
tagsReadonlyArray<string>yesOpenAPI tags.

HttpOperationMetadata, the argument to annotator:

FieldTypeOptional
operationIdstringyes
summarystringyes
descriptionstringyes
tagsReadonlyArray<string>yes

Absent fields are omitted, not emitted as undefined. HttpOperationAnnotator is (metadata: HttpOperationMetadata) => Context.Context<never>.

ExportedFunctionSymbol is (request: never) => CommandIdentityValue:

FieldTypeOptional
keystringno
inputobjectno
preconditionstring or nullno

The application owns key issuance, the original request and precondition, and retry. effx never mints a key, holds an identity in hidden state or retries (spec 0007 §"Stable command identity").

Source: packages/runtime/src/Annotation.ts (HttpOperationMetadata, HttpOperationAnnotator, CommandIdentityValue, ExportedFunctionSymbol); docs/specs/0010-target-profiles-and-emit-modes.md.

Query over POST: payloadIsQuery

A @Query may use POST only when its contract sets payloadIsQuery: true and declares a payload. The flag exists for read inputs that must not appear in a URL, such as an exact email address. The body stays in the request body, not in the URL or query channel.

Illustrative snippet (the names are invented; the options are the real ones):

@Query({ name: "Org.LookupCandidate", input: LookupInput, success: Candidate })
@Http.Post("/api/lookup")
@Http.Contract({ group: "org", payload: LookupInput, payloadIsQuery: true, success: Candidate })
CaseResult
Query, POST, payload present, payloadIsQuery: trueallowed
flag on a Command, on a non-POST method, or without payloadEFFX2402
Query over any other non-GET exposure, or POST without the flagEFFX2401

This is an opt-in declaration of intent. It is not permission to write, not an authorization decision, and not a purity guarantee. The handler stays responsible for read-only semantics (ADR 0010, docs/decisions/0010-query-over-post.md; ADR 0006).

Request channels and query: true

params, query, headers and payload are derived from the operation's input when you omit them, and an explicit channel always wins (How group defaults merge). An input wrapped by Http.headers(schema) is the headers channel. The derived channel is written into this contract before interpretation, so the IR, paramsKeys/headersKeys and generated files equal the explicit spelling.

query: true still means "the operation's declared input is the query". It is redundant for a GET whose route has no path parameters, and resolves only through a group association: it is EFFX2405 when the method or kind is wrong, input is already assigned to params or headers (or is a header schema), or a payload is declared. The IR option schema accepts a schema for query, so without an association true is not rewritten (group-defaults.ts, http-contract.ts).

Middleware and security markers

middleware entries must be exported HttpApiMiddleware.Service marker classes. The frontend records a security flag for markers that carry the HttpApiMiddleware security type stamp. That is what satisfies EFFX2503 for a protected Http.Access. Root schema-error middleware cannot. The compiler checks presence and identity only, not that a Layer implements the marker.

Diagnostics

CodeCondition
EFFX2402duplicate Http.Contract
EFFX2402the operation does not have exactly one HTTP exposure
EFFX2402payloadIsQuery outside a Query over POST with a payload
EFFX2402GET with an explicit payload
EFFX2402conditional on a non-GET or without responseHeaders
EFFX2402mediaType without payload
EFFX2402status outside 100 to 599
EFFX2402unsafe root or group identifier; root export name collision
EFFX2402params fields differ from the route parameters, or one exists without the other
EFFX2402commandIdentity on a non-Command, or headers missing idempotency-key or if-match
EFFX2402external group without @Http.Group, or without a concrete HttpApi root
EFFX2403operationId not <group>.<identifier-safe key>; duplicate endpoint key; missing for an external binding; mixed local and external bindings in a group
EFFX2410input mixes path parameters and other keys, or is a header schema next to a different headers (see derived channels)
EFFX2411GET/DELETE with path parameters and an input whose keys are not static, while neither params nor query is written

Source: packages/compiler/src/extensions/http-contract.ts (validate); EFFX2410 and EFFX2411 come from the pre-pass packages/compiler/src/request-channels.ts. A property is never silently dropped: a bad combination is a diagnostic before generation.

Generated shape

Per spec 0005 §"Generation and compatibility": operations group by (root, group), one group class per pair and one root class per root. Endpoint options use Effect v4 params, query, headers, payload, success and error. Status and media wrappers come from HttpApiSchema. Middleware and OpenApi.annotations attach after the endpoint is built. The handler is an ordinary Effect handler. This page does not claim the exact emitted text; inspect it with effx build.

On this page