001. One contract, no client code
The question this chapter answers: how does a loader call a service it has never seen a client for, with the editor knowing the types and the runtime refusing a bad call?
The document is the integration
A service the application talks to already describes itself. An HTTP service publishes an OpenAPI document; a gRPC service publishes a .proto. fsr treats that document as the whole of the integration: drop it under app/clients/ and the build imports it into the contract. The storefront has two, shopping.openapi.json for the catalog and orders and inventory.proto for stock levels. Nothing under app/ mentions HTTP or gRPC anywhere.
The name of the file is the name of the service. shopping.openapi.json is services.shopping; each operation's operationId is a method; each schema is a type. For a proto, the one service in the file takes the file's name, the messages become records and int64 stays a 64-bit integer rather than becoming a JavaScript number, which is why the value model has a bigint and the generated types say so.
A backend that keeps its document honest is integrated. That is the promise to the backend developer: publish what you serve and the frontend team is done. The storefront's HTTP backend goes further and includes its document from app/clients/ at compile time, so the document the build imports and the document the server publishes are the same bytes and cannot disagree.
What the build makes of it
The build writes one contract file per document under generated/contracts/, plus one for the application's own schemas, then merges them at boot. A type or a service defined twice names both files, since two teams' documents landing in one directory is exactly when that matters.
From the merged contract it writes generated/services.d.ts, the TypeScript the editor sees:
services.shopping.listProducts(args: { q?: string; category?: string; tag?: string }): Promise<Product[]>;
services.inventory.getStock(args: { product_id: bigint }): Promise<StockLevel>;A loader calls a service through ctx.services with exactly that shape. The call is a row in the plan file: service, method, arguments. There is no client module because there is nothing a client module would add. The transport is chosen by the host from the document's extension and the base URL in configuration, which is chapter 202.
The runtime checks both directions
At request time the registry checks every call's arguments against the contract before the transport sees them and every response after it. A loader that sends a string where the document says integer fails with the field's path. A backend that answers with a missing required field fails the same way, naming the method, rather than letting a wrong shape reach a page and render as undefined.
This is the reason the registry exists as a block rather than as generated code. Generated clients check what they were generated from; the registry checks the document that is deployed. When a backend changes its document, the next build changes the types and the next boot changes the checks. Nothing in between has to be regenerated by hand.
Why this is the enterprise argument
A team with forty services has forty documents already. What it usually lacks is one place where a frontend's use of them is typed, checked and visible. fsr makes the contract that place: the editor types against it, the build refuses an unknown method, the runtime refuses a bad shape, the boot report lists every service and its transport. The cost of adding a service is copying its document. The cost of a service changing is a build that says where.
The lab
Open shopping.openapi.json and rename price_cents in the Product schema. Run fsr check app: the loaders still lower, since they pass the product through, but tsc over the app fails in every page that reads the price, because the generated types moved. Put it back.
Now leave the document alone and change the backend: in catalog.rs make a product's rating a string. Boot and load the catalog. The page renders the error component and the message names shopping.listProducts() and the field, because the response failed the contract before anything else saw it.