Type to search pages and rules.

Browse pages

Frontend Data and State

A LiteSpec frontend owns no API contract: it calls the API through one generated client, reads answers through the client's cache, and sends each write as a command, one message that changes business state. This page states how the client is built and configured, where each piece of state lives, and how every failure reaches the reader as one problem, a project-owned failure shape carrying a stable code. Frontend developers and agents apply it for every read, write, and form, so a contract change breaks the build instead of the running page and every failure renders a sentence the team wrote for it.

Markdown23 provisions

Stack: any Layer: frontend-data

Concepts#

The Generated Client#

The API publishes one OpenAPI document. Generation turns it into the client the frontend calls: typed operations, request and response types, validation schemas, and the operation options each read and write needs. Because the output is generated, a contract change becomes a compile error in every application at once instead of a wrong request at runtime. The workspace configures the client once per application, with its base address, credentials, headers, and failure mapping, and every read and write passes through it.

The frame is the part of a frontend that holds everything cross-cutting, such as the client configuration and shared helpers (Frontend Structure).

Where State Lives#

Every value the frontend holds has one owner, from the widest to the narrowest: server data in the query cache, shareable values in the address, input in form state, and the rest in the component's own state. A value kept above its narrowest owner forces unrelated components to re-render and hides where the value changes.

The Three State Classifications#

Durable state is what the user made: persisted, versioned, and exported. The expense draft is durable state, because the user built it and it must survive a restart and an export. Device-local state is persisted but stays on the device, excluded from export. Derived state is computed from durable state and never persisted: an expense total is derived from the expense lines each time it is read.

Outcomes of a Command#

Every command ends in one outcome. A page handles each outcome as the table states.

OutcomeWhat the page does
SuccessInvalidates the queries the command changes, then says so where nothing else on screen shows it
RefusalRenders the refusal beside what was refused, in the sentence the application holds for that code
Field refusalPlaces each problem on the field it belongs to, leaving the form open

Success carries a visible answer wherever nothing else on screen shows it; a silent success tells the reader nothing happened. Field problems belong on their fields; a list above a long form forces the reader to match each sentence to a label.

The Problem Type#

Whatever produces a failure, whether a refused command, a failed read, or a network error, the frontend converts it into one project-owned problem carrying the same fields.

The problem carries a status, a code, a detail, an optional trace identifier, and field problems. Each field problem names its field and carries its own code and message.

The code is the stable identifier the page branches on, so a reworded message leaves the page's decision unchanged. A code this build does not recognize is still handled: the page renders the problem by the code's presence rather than by matching a list of known codes.

The problem carries the trace identifier the API returns for the request, so support can find the failed request. Every visible error shows that identifier in copyable form in its detail; the headline carries what happened in words.

Standards#

Generate the Client from the Published Contract#

ruleruleguidance
litespec/frontend/data#generate-the-client-from-the-published-contract
Statement

A frontend MUST generate its client, types, schemas, and operation options from the API's published OpenAPI document.

Applies toartifact: any

Why

A handwritten copy of a request, a response, or a schema drifts from the contract silently: the API changes, the copy still compiles, and the page sends a request the API no longer accepts. A generated client turns the same contract change into a compile error in every application at once, at every call site the change affects.

Checka reviewer answers

Reviewed forWhich client, type, schema, or operation option is handwritten instead of generated from the published OpenAPI document? Look in the generated client directory and the contract it names.

Marked guidance: a reviewer answers this question.

Use One Configured Client per Application#

ruleruleguidance
litespec/frontend/data#use-one-configured-client-per-application
Statement

A frontend MUST configure the generated client once, and route every read and write through it.

Applies toartifact: component, route

Why

The configured client owns the base address, the credentials, the headers, and the mapping from a failure to a problem. A call that builds its own request opts out of all four at once: it can point at the wrong host, miss the session cookie, skip the write marker, and surface a raw failure outside the page's problem shape. Routing every read and write through the one client keeps those four decisions in one place.

Checka reviewer answers

Reviewed forWhich reads or writes bypass the one configured client? Look in the frame client configuration and the call sites.

Marked guidance: a reviewer answers this question.

ruleruleguidance
litespec/frontend/data#keep-the-session-in-an-api-owned-cookie
Statement

The API MUST hold the browser session in an HTTP-only cookie, with frontend code holding no token.

Applies toartifact: any

Why

Every value a script can read is a value any script on the page can read, including a script an attacker injected. Tokens kept in frontend code or browser storage are therefore readable by the same attack the session is meant to resist. The API runs the authorization code flow and keeps the tokens encrypted in an HTTP-only cookie the browser sends on its own, so frontend code holds no token. A refused sign-in lands on a signed-out page rather than on an error, because the reader is not signed in.

Checka reviewer answers

Reviewed forWhich tokens sit in frontend sources instead of the API-owned HTTP-only cookie? Look in the API session handling and the frontend sources.

Marked guidance: a reviewer answers this question.

Set the Write Marker on Every Request#

ruleruleguidance
litespec/frontend/data#set-the-write-marker-on-every-request
Statement

A client MUST send its marker header on every write, with the API accepting a cookie-authenticated write only when it carries that header.

Applies toartifact: component, route

Why

A browser attaches its cookies to a cross-site request on its own, so the cookie alone fails to distinguish the page's own write from a forged post submitted from another site. The marker header separates the two: the page's client adds it to every write, and a page on another site adds no custom header without a preflight, which the API grants to no foreign origin. The API therefore accepts a cookie-authenticated write only when the header is present. The header name varies by project, so configuration owns it rather than code; it has the form X-<Project>-Request with the value 1.

Checka reviewer answers

Reviewed forWhich writes lack the configured marker header of the form X-<Project>-Request? Look in the client configuration and the API write acceptance.

Marked guidance: a reviewer answers this question.

Map Every Failure to One Problem Type#

ruleruleguidance
litespec/frontend/data#map-every-failure-to-one-problem-type
Statement

A frontend MUST convert every failure into one problem type, deciding from its code and showing its trace identifier in copyable form.

Applies toartifact: component, route

Why

A raw response body carries whatever the API included: field names, internals, sometimes data kept from the reader. Rendering it leaks that content and gives the reader no consistent recovery path, because each failure looks different. One problem type gives every failure the same shape: the page branches on the published code, so its decision survives a reworded message, and the trace identifier in the detail lets support find the request the reader saw fail. The identifier stays out of the headline, which says what happened in words the team wrote.

Checka reviewer answers

Reviewed forWhich failures render outside the one problem type, or hide the trace identifier from the detail? Look in the frame problem mapping and the error surfaces.

Marked guidance: a reviewer answers this question.

Place a Refusal Beside What Was Refused#

ruleruleguidance
litespec/frontend/data#place-a-refusal-beside-what-was-refused
Statement

A refusal MUST render beside the record or control that was refused, in the sentence the application holds for that code.

Applies toartifact: component, route

Why

A refusal at the top of a page says that something failed but not what, so the reader scans the page to find the control that caused it. Rendered beside the refused record or control, the refusal answers both questions at once. The sentence comes from the application's own dictionary for that code rather than from the response body, so the words stay consistent and translatable; the code in the markup lets a test provoke the refusal by name.

Example

Submitting an expense above the receipt threshold with no receipt refuses with spending.receipt-missing under result/submit-expense.receipt-missing. The page renders the application's sentence for that code beside the Submit control, carries the code in the markup so a test provokes it by name, and keeps the trace identifier in the detail in copyable form.

Checka reviewer answers

Reviewed forWhich refusals render away from the refused record or control, or without the application's sentence and code in the markup? Look in the screen's page files beside the refusing controls.

Marked guidance: a reviewer answers this question.

Place a Field Refusal on Its Field#

ruleruleguidance
litespec/frontend/data#place-a-field-refusal-on-its-field
Statement

A field problem MUST reach the field it names through the frame's one helper, with the form staying open until resolved or abandoned.

Applies toartifact: form

Why

A list of problems above a long form forces the reader to read each sentence and each label to pair them, and a problem below the fold is missed entirely. The frame's one helper puts each problem on the field it names, so the reader sees what to fix where they fix it. The form stays open until each problem is resolved or the reader abandons the form, so no input is lost to a closed dialog.

Checka reviewer answers

Reviewed forWhich field problems miss their fields, bypass the frame helper, or close the form? Look in the forms and the frame helper.

Marked guidance: a reviewer answers this question.

Validate a Form with the Generated Schema#

ruleruleguidance
litespec/frontend/data#validate-a-form-with-the-generated-schema
Statement

A form MUST validate against the generated schema for its request body, extended with the application's own rules rather than replaced by them.

Applies toartifact: form

Why

The generated schema is the contract's statement of what the API accepts, so it holds even where the browser accepts more. Validating the form against it refuses bad input before the request leaves the page. The application extends the schema with the message shown for each rule and any rule beyond the contract's reach, rather than replacing it, so the form refuses everything the API refuses.

Checka reviewer answers

Reviewed forWhich forms validate against a handwritten restatement instead of the generated request schema extended with application rules? Look in the form sources and the generated schemas.

Marked guidance: a reviewer answers this question.

Update the Cache a Command Changed#

ruleruleguidance
litespec/frontend/data#update-the-cache-a-command-changed
Statement

A successful command MUST invalidate every query whose answer it changed, and no other query.

Applies toartifact: component, route

Why

The query cache owns freshness: a page renders the cached answer, not a new request. A command that changes a record but leaves its cache entry therefore shows the old record until something else happens to refetch, so the reader's own write appears to have no effect. Invalidating exactly the queries the command changed refreshes what changed; invalidating more costs a needless request per affected page.

Example

The generated client holds both sides of one write: the expenses route calls the submit operation with the expense values, and the list reads through the list operation. After the submit succeeds, the page invalidates the list query and no other query, so the list shows the new expense without refetching unrelated pages.

Checka reviewer answers

Reviewed forWhich successful commands leave a changed query cached, or invalidate a query their answers left unchanged? Look in the command call sites and the cache keys.

Marked guidance: a reviewer answers this question.

Assign State to the Narrowest Owner#

ruleruleguidance
litespec/frontend/data#assign-state-to-the-narrowest-owner
Statement

A value MUST live with the narrowest owner that can hold it: server data, then the address, then form state, then the component's own state.

Applies toartifact: component, route, form

Why

Server data belongs in the query cache, shareable values in the address, input in form state, and the rest in the component's own state. A value kept above its narrowest owner, typically in a shared store, makes unrelated components re-render when it changes and hides where the change came from. The order gives each value the smallest audience that can use it.

Checka reviewer answers

Reviewed forWhich values live above their narrowest owner instead of the query cache, the address, form state, or component state? Look in the state declarations.

Marked guidance: a reviewer answers this question.

Route Use Cases Through Application#

ruleruleguidance
litespec/frontend/data#route-use-cases-through-application
Statement

A frontend MUST invoke an Application handler carrying a Command or Query role suffix for each use case rather than compose Domain calls or write storage.

Applies toartifact: component, route

Why

The handler is the one place a use case exists: its validation, its rules, and its result. A screen or component that composes its own sequence of Domain calls and storage writes creates a second definition that no record names and no test pins down. Routing every use case through a handler keeps the screen or component a caller and the handler the definition.

Example

The expense screen invokes the submit-expense Application handler for EXP-1042 rather than composing the Domain expense calls and writing the expense store itself.

Checka reviewer answers

Reviewed forWhich screens or components compose Domain calls or write storage instead of invoking an Application handler carrying a Command or Query role suffix? Look in the screen and component sources.

Marked guidance: a reviewer answers this question.

Classify State#

ruleruleguidance
litespec/frontend/data#classify-state
Statement

An aggregate introducing state MUST classify it as durable, device-local, or derived.

Applies toartifact: aggregate

Why

The classification decides what happens to the state: durable state is persisted, versioned, and exported, device-local state is persisted but excluded from export, and derived state is never persisted. Without a declared classification each reader guesses, and a value the user made can end up unpersisted while a computed value ends up stored and stale.

Example

The expense draft for the Rotterdam client visit is durable state, the device copy of the receipt photo is device-local state, and the expense total for EXP-1042 is derived state computed from its lines.

Checka reviewer answers

Reviewed forWhich aggregates introducing state name no durable, device-local, or derived classification on their page? Look on the aggregate pages.

Marked guidance: a reviewer answers this question.

Route a Personal Value in a Header#

ruleruleguidance
litespec/frontend/data#route-a-personal-value-in-a-header
Statement

A value naming a person MUST travel in a request header rather than the query string, with the contract declaring the header each operation reads.

Applies toartifact: component, route

Why

A query string leaks: proxies log it, the browser keeps it in history, and the next page the reader opens receives it as the referrer. A value naming a person therefore travels in a request header the contract declares per operation, where it reaches the API without landing in those three places. The address keeps its own copy for filters and paging, so a filtered list stays a link somebody can share.

Checka reviewer answers

Reviewed forWhich person-naming values travel in the query string instead of contract-declared headers? Look in the contract headers and the call sites.

Marked guidance: a reviewer answers this question.

Read a Permission from the Session#

ruleruleguidance
litespec/frontend/data#read-a-permission-from-the-session
Statement

A page MUST offer a command only when the session actor holds its permissions at the record's scope, with the API deciding every command.

Applies toartifact: route

Why

Permissions are the project's permission records: an actor holds a role at a scope, a role allows permissions, and a use case needs permissions. The page reads the permissions the signed-in actor holds from the session the API returns, and offers only the commands those permissions allow at the record's scope, so the reader meets no offer that ends in a refusal. The API still decides every command, so an action offered in error is refused rather than executed.

Checka reviewer answers

Reviewed forWhich offered commands exceed the permissions the session actor holds at the record scope? Look in the page command guards and the session shape.

Marked guidance: a reviewer answers this question.

Name the Zone for Every Timestamp#

ruleruleguidance
litespec/frontend/data#name-the-zone-for-every-timestamp
Statement

A formatter rendering a timestamp MUST receive an explicit IANA time zone, resolved by the caller from the record the screen shows.

Applies toartifact: component, route

Why

An instant from the API carries no zone, so a formatter left to pick one shows the reader's own zone: a payout cut-off reads as midnight in the reader's city instead of the office's. The caller resolves the zone from the record the screen shows: a record with its own zone states it, otherwise the account's zone applies, and an account that chooses none falls back to UTC. Passing the zone explicitly makes each timestamp name whose day it belongs to.

Checka reviewer answers

Reviewed forWhich timestamp formatters run without an explicit IANA zone resolved from the shown record? Look in the formatter call sites.

Marked guidance: a reviewer answers this question.

Keep Secrets Out of Browser Storage#

ruleruleguidance
litespec/frontend/data#keep-secrets-out-of-browser-storage
Statement

A frontend MUST NOT store a refresh token, a provider secret, or a privileged credential in browser storage or a browser-visible variable.

Applies toartifact: any

Why

Local storage, session storage, IndexedDB, and a build-time public variable are all readable by any script on the page, so a secret kept there is available to the same injected script the secret is meant to resist. Refresh tokens, provider secrets, and privileged credentials therefore stay out of the browser entirely. A preference the reader chooses, such as a theme or a locale, is not a secret and can be stored under a named key in the frame.

Checka reviewer answers

Reviewed forWhich refresh tokens, provider secrets, or privileged credentials sit in browser storage or browser-visible variables? Look in the storage writes and the public variables.

Marked guidance: a reviewer answers this question.

Make Optimistic Behavior Recoverable#

ruleruleguidance
litespec/frontend/data#make-optimistic-behavior-recoverable
Statement

An optimistic update MUST declare its stable client identity, its conflict behavior, its failure rollback, and its reconciliation path.

Applies toartifact: component, route

Why

An optimistic update shows a value the server has not confirmed. Without a stable client identity the retry after a failure creates a duplicate; without declared conflict behavior two writers overwrite each other silently; without a rollback path a failed update leaves the shown value wrong. Declaring identity, conflict behavior, rollback, and reconciliation up front makes each of those cases designed instead of improvised. Money, irreversible actions, and uncertain authorization skip optimism entirely and wait for the server result.

Checka reviewer answers

Reviewed forWhich optimistic updates lack a declared client identity, conflict behavior, rollback, or reconciliation path, and which money, irreversible, or uncertainly authorized actions run optimistically instead of waiting for the server? Look in the optimistic update definitions.

Marked guidance: a reviewer answers this question.

Announce the Outcome of an Optimistic Update#

ruleruleguidance
litespec/frontend/data#announce-the-outcome-of-an-optimistic-update
Statement

An optimistic update MUST announce its reconciled outcome through a live region when the server result differs from the value shown.

Applies toartifact: component, route

Why

A rollback is a silent visual change: the shown value changes back with no event a screen reader announces. The reader heard the optimistic value confirmed and hears nothing when it is withdrawn, so they act on a value the server refused. Announcing the reconciled outcome through a live region tells them what actually holds. The ARIA live-region technique is the mechanism, and a test of the route asserts the announcement.

Checka reviewer answers

Reviewed forWhich reconciled outcomes differing from the shown value announce through no live region? Look in the route tests asserting the announcement.

Marked guidance: a reviewer answers this question.

Localize Copy Through Locale Dictionaries#

ruleruleguidance
litespec/frontend/data#localize-copy-through-locale-dictionaries
Statement

Copy MUST resolve through locale dictionaries with fill and plural support, one chunk per language, and typed keys with a declared fallback.

Applies toartifact: component, route, form

Why

One sentence written in two components drifts at the first rewording, because the author finds one copy and misses the other. A dictionary per language keeps each sentence in one place, keyed by a typed key the compiler checks, with fill and plural support so variable content and counts render correctly in every language. One chunk per language keeps a reader's download to the language they read, and a key the catalog lacks fails where the declared fallback says it fails instead of rendering a raw key.

Example

A workspace that carries its own dictionary machinery still meets the same contract.

Checka reviewer answers

Reviewed forWhich copy bypasses the locale dictionaries, or misses fill, plural, chunking, typed keys, or a declared fallback? Look in the locale dictionaries and the copy call sites.

Marked guidance: a reviewer answers this question.

Conventions#

Keep the Client Boundary in the Frame#

conventiondefaultguidance
litespec/frontend/data#keep-the-client-boundary-in-the-frame
Default

Place the client configuration, problem mapping, and download helpers in the frame root, keeping them out of every use-case folder.

ReplacementA consumer can replace this default with an explicit local convention.

Applies toartifact: any

Why

The client configuration, the problem mapping, and the download helpers serve every use case, so a copy in each use-case folder turns one change to the base address or the headers into an edit per folder, with the missed folder failing at runtime. The frame root holds them once, where every use case reaches them and no use case duplicates them.

Checka reviewer answers

Reviewed forWhich client configuration, problem mapping, or download helpers sit in a use-case folder instead of the frame root? Look in the frame root and the use-case folders.

Marked guidance: a reviewer answers this question.

Regenerate the Client Rather Than Editing It#

conventiondefaultguidance
litespec/frontend/data#regenerate-the-client-rather-than-editing-it
Default

Regenerate the client from the document, and edit nothing under the generated directory.

ReplacementA consumer can replace this default with an explicit local convention.

Applies toartifact: any

Why

A hand edit under the generated directory works until the next generation overwrites it, so the change appears sound and then silently disappears. Regenerating from the document keeps the committed output provably equal to the contract: a reviewer regenerates and compares, and any difference is either a contract change or an edit belonging outside the generated directory.

Checka reviewer answers

Reviewed forWhich hand edits sit below the generated directory, making the committed client differ from fresh generation output? Look in the generated directory and the generator command.

Marked guidance: a reviewer answers this question.

Keep Schemas Use-Case Specific#

conventiondefaultguidance
litespec/frontend/data#keep-schemas-use-case-specific
Default

Keep a form schema or a view model in its owning use-case folder until a second use case needs it.

ReplacementA consumer can replace this default with an explicit local convention.

Applies toartifact: any

Why

A schema shared before its second use case exists is a contract written for one reader. The second use case then either contorts itself to fit or breaks the first reader by changing the shared shape. Keeping a form schema or view model in its owning use-case folder until a second use case needs it lets the shared shape emerge from two real readers.

Checka reviewer answers

Reviewed forWhich form schemas or view models sit in a shared location with only one use case reading them? Look in the use-case folders and the shared schema locations.

Marked guidance: a reviewer answers this question.

Build a Form on the Shared Form Library#

conventiondefaultguidance
litespec/frontend/data#build-a-form-on-the-shared-form-library
Default

Build a form on the shared package's form entry point and its registered field components.

ReplacementA consumer can replace this default with an explicit local convention.

Applies toartifact: form

Why

A form library used directly in every use-case folder gives each folder its own fields, its own validation wiring, and its own error display, and none of them knows how to show a refusal on its field. The shared package's form entry point and registered field components hold those decisions once, so every form validates, renders, and refuses the same way.

Checka reviewer answers

Reviewed forWhich forms bypass the shared package form entry point or its registered field components? Look in the form sources and the shared package form entry point.

Marked guidance: a reviewer answers this question.

Reference Example#

This informative example demonstrates litespec/frontend/data#place-a-field-refusal-on-its-field, litespec/frontend/data#update-the-cache-a-command-changed, and litespec/frontend/data#validate-a-form-with-the-generated-schema.

A redelivery dialog holds one field. Submitting validates against the generated schema and applies any field problem back to that field. Success invalidates the expense it changes and closes the dialog.