Core concepts
A short tour of the model behind TruSpec. Once these click, the file format and CLI read as a thin surface over a few ideas.
The four principles
Everything in TruSpec follows from four commitments:
- Open-source first — no feature paywalls, no mandatory account.
- Local-first — everything works offline; your files are the source of truth.
- Agent-native — every capability is reachable three ways: plain files, a
--jsonCLI, and an MCP server. No capability is locked behind a GUI. - Refuse bloat — speed and focus are the product. Dashboards, flow builders, mandatory cloud sync, and exotic protocols are deliberately out.
Collections are plain text
A collection is a folder of YAML files. There is no binary database, no proprietary export format, and no hidden state — the files are the collection.
| File | Purpose |
|---|---|
<name>.tspec.yaml | One HTTP request, its assertions, and its spec link. |
folder.tspec.yaml | Config inherited by every request in that folder and below. |
environments/<name>.env.yaml | Variables and secret names for one environment. |
Because they're plain text, they diff cleanly in Git, review well in a pull request, and can be authored by a human, a script, or an AI agent with equal ease. The hard rule that makes this work: TruSpec validates before it writes, so a malformed file never lands in your repo.
The spec is the source of truth
Most API clients treat the collection as the canonical artifact and the spec (if any) as a side note. TruSpec inverts that: your OpenAPI document is the source of truth, and the collection is measured against it.
A request links back to a spec operation with a spec: block:
spec:
operation: "GET /pets/{id}" # `${METHOD} ${path}`
operationId: getPetById # preferred when presentThat link powers two checks that run offline and in CI:
- Drift — what's in the spec but untracked by any request, what's referenced but no longer in the spec, and what's matched but no longer satisfies the spec (e.g. a now-required parameter).
- Coverage — what share of spec operations are actually exercised by a request with assertions.
This is the feature that distinguishes TruSpec: your collection can't silently rot away from your API, because the build fails the moment it does.
The workspace
The workspace is the root TruSpec resolves environments and folder config from. You never configure it explicitly — it's discovered by walking up the directory tree from the request you're running until it finds a directory containing an environments/ folder or a .git directory.
That means a collection nested anywhere inside your repo still resolves the right environments, and a root .env is found regardless of which subfolder you run.
Folder inheritance
folder.tspec.yaml files compose from the workspace root down to the request's folder. Deeper files win. This lets you set a base URL or shared headers once:
# api/folder.tspec.yaml
name: My API
baseUrl: "{{baseUrl}}"
headers:
Accept: application/json
auth:
type: bearer
token: "{{token}}"Merge rules:
baseUrl,auth,name— the deepest value replaces shallower ones.headers— merged key by key (a child can add or override individual headers).- At request time, the request's own
headers/auth/urlwin over inherited config, and a relative requesturlis joined onto the inheritedbaseUrl.
See File format → Folder config for the full rules.
Variables and secrets
Any string can contain {{name}} templates, resolved at run time. Values come from several sources, resolved in this order (later wins):
variables:in the active environment file.- Secrets declared by name in the environment, resolved from a workspace
.envfile and then from real OS environment variables (OS wins). - Values captured from earlier requests in the same run.
Secrets are never stored in your files — the environment lists only their names:
# environments/staging.env.yaml
name: staging
variables:
baseUrl: "https://api.staging.example.com"
secrets:
- token # value comes from $token (OS env) or a .env file, never from hereWhen a run reports its results, declared secret values are masked with *** in URLs, bodies, headers, captured values, and error messages — so --json output and CI logs don't leak them. See File format → Environments.
How a run flows
When you truspec run, each request goes through the same pipeline:
parse → pre-script → resolve → fetch → assert → capture → post-script- Parse the YAML and validate it against the schema.
- Pre-request script (optional) runs first and can set variables. See Scripting.
- Resolve — interpolate
{{variables}}, apply folder inheritance, build auth headers, assemble the query string and body into a concrete HTTP request. Unresolved variables fail the request before anything is sent. - Fetch the request (default 30s timeout; response body capped to guard memory).
- Assert — evaluate the declarative assertions against the response.
- Capture — save response values into variables for later requests.
- Post-response script (optional) can add assertions and capture more values.
Requests in a directory run in order (ascending, then by file path), so a login can capture a token the next request consumes. The run exits non-zero if any request fails — which is what makes the same command work locally and as a CI gate.
The engine, the CLI, and the agent surface
The same engine powers every entry point, so behavior is identical no matter how you drive it:
@truspec/core — the engine (pure TypeScript)
├─ format parse / serialize / validate (+ published JSON Schema)
├─ runner interpolation, auth, fetch, declarative assertions, scripts
├─ workspace discovery, folder inheritance, env + secret resolution
├─ spec OpenAPI drift + coverage + response contract validation
├─ importers Postman v2.1 + Bruno → .tspec.yaml
└─ mock local mock server generated from a spec
truspec — the CLI
@truspec/mcp-server — the agent surface (11 MCP tools)
@truspec/web — the local web UI (truspec serve)Run it from your terminal, your CI, your editor, or your AI agent — it's the same code underneath. Read on:
- File format — the complete schema reference.
- CLI — every command and flag.
- Programmatic API — drive the engine from TypeScript.