Work across services
Run one extract per service into the same folder, then one check over the folder.
npx @suss/cli extract -p services/catalog/tsconfig.json -f hono -o summaries/catalog.json
npx @suss/cli extract -p apps/storefront/tsconfig.json -f fetch -o summaries/storefront.json
npx @suss/cli check --dir summaries/suss pairs summaries by boundary, so it compares a handler in one service with the client that calls it in another, whichever run wrote them. Order does not matter, because check reads whatever is in the folder. Each team writes the extract command for its own service. The pipeline runs all of them.
Two repos
When the two sides live in separate repositories, the provider writes its summaries to a file its repo checks in:
# in the catalog-api repo
npx @suss/cli extract -p tsconfig.json -f hono -o suss/catalog.json
git add suss/catalog.jsonThe consumer copies that file in next to its own and checks the pair:
# in the storefront repo
cp ../catalog-api/suss/catalog.json summaries/
npx @suss/cli extract -p tsconfig.json -f fetch -o summaries/storefront.json
npx @suss/cli check --dir summaries/extract writes file paths relative to the project it read, and the format has nothing machine-specific in it, so the file means the same thing in the consumer's repo as it did in the provider's. Publish summaries shows how to ship the file inside the package instead of copying it.
You can also pass both files, when the folder has more in it than the pair you want:
npx @suss/cli check summaries/catalog.json summaries/storefront.jsonWhen the provider stops sending a field
The catalog service used to send a discounted price and no longer does:
// catalog-api/src/api.ts
app.get("/products/:sku", async (c) => {
const product = await lookup(c.req.param("sku"));
if (!product) {
return c.json({ error: "no such product" }, 404);
}
return c.json({ sku: product.sku, title: product.title, price: product.price });
});The storefront still reads it:
// storefront/src/product.ts
export function loadProduct(sku: string) {
return fetch(`/products/${sku}`)
.then((res) => res.json())
.then((product) => product.discountedPrice);
}Neither repository imports the other, and no type connects them. check reports it anyway:
Compared 1 boundary.
────────────────────────────────────────────────────────────
[WARNING] unhandledProviderCase
Provider produces status 404 but no consumer branch handles it
provider: src/api.ts::get (src/api.ts:5)
consumer: src/product.ts::loadProduct (src/product.ts:1)
boundary: hono (http) GET /products/:sku
to silence this one, add to the rules in .sussignore.yml:
- kind: unhandledProviderCase
boundary: "GET /products/{sku}"
provider: { transitionId: "get:response:404:9cb77da" }
reason: TODO say why you accept this
────────────────────────────────────────────────────────────
[ERROR] misreadProviderResponse
The consumer's fall-through path reads "discountedPrice", but the 200 body the provider sends does not include it, and neither does any other response.
provider: src/api.ts::get (src/api.ts:5)
consumer: src/product.ts::loadProduct (src/product.ts:1)
boundary: hono (http) GET /products/:sku
to silence this one, add to the rules in .sussignore.yml:
- kind: misreadProviderResponse
boundary: "GET /products/{sku}"
provider: { transitionId: "get:response:200:fed8467" }
reason: TODO say why you accept this
────────────────────────────────────────────────────────────
2 findings: 1 error, 1 warning, 0 info
suss met a call it could not follow in one unit, of 2, so that one is described in part. `suss inspect` says which calls.The error gives a file and a line for each side, so the fix is a two-line diff in whichever repo was wrong. The warning above it comes from the same pair, seen from the other direction. The catalog service returns 404, and the storefront has no branch for it.
Set them all up at once
At a repo root, init reads the workspace declaration, from package.json workspaces, pnpm-workspace.yaml, lerna.json or turbo.json, and reports on each package under it:
════ services/catalog ════
✓ Found 3 things to read
Your code
hono hono in dependencies
fetch TypeScript sources, and fetch reads what the language itself ships
What your code reaches
node TypeScript sources, and node reads what the language itself ships
1. Install suss
npm install --save-dev @suss/cli
2. Read each side into one folder
suss extract -f hono -f fetch -f node -o summaries/code.jsonYou get one block per package, each with the extract command that package needs. Without --plain, init asks which packages to set up and then writes one suss.json for all of them. When the output is piped or the run is in CI, it prints the commands instead.
Two services that serve the same path
When two services both serve GET /users, suss sees one boundary. A client inside one of those services is compared with its own service's route. A client anywhere else could reach either one, so check compares it with neither and says so:
[WARNING] ambiguousProvider
web::src/users.ts::loadUsers calls GET /users, and 2 services serve it (auth, directory). Nothing here says which one it reaches, so no pair was checked. Give the client the base URL it calls, or check one service at a time.
provider: src/api.ts::get (src/api.ts:3)
consumer: src/users.ts::loadUsers (src/users.ts:1)
boundary: fetch (http) GET /usersTo compare that client, check it against one service at a time:
npx @suss/cli extract -p services/auth/tsconfig.json -f hono -o auth/api.json
cp summaries/web.json auth/
npx @suss/cli check --dir auth/Two GraphQL services behind one frontend
A frontend that sends some operations to one GraphQL service and some to another usually builds two Apollo clients and picks one per call. suss reads which client each call picks, through the project's own hook too. Tell it which service each client talks to, by the endpoint the client is built with, in the Apollo pack's options:
{
"clients": {
"/graphql": "schema/legacy.graphql",
"https://events.appsync-api.example.com/graphql": "schema/appsync.graphql"
}
}Each operation then pairs with its own service's schema. An operation whose client suss cannot tie to one service is listed as not checked, with the reason, instead of being compared with a schema it may never reach. Set "unboundClients": "pairWithAll" in the same options to compare such an operation with every schema that has its fields instead, with a warning that says the match is ambiguous. The Apollo pack shows the spellings it reads.
A spec instead of the other side's code
suss contract reads a spec and writes it out in the same format as extract. The other side can then be an OpenAPI document, a Prisma schema or a CloudFormation template instead of code:
npx @suss/cli contract --from openapi ../catalog-api/openapi.yaml -o summaries/catalog.json
npx @suss/cli check --dir summaries/In Check against OpenAPI, suss compares the spec, the handlers behind it and the client that calls it in one run. Contract sources lists what --from accepts.