open source · one Rust core · MIT
API specsthat run.
Describe every endpoint in plain .outry files next to the code. Run them from the terminal, your editor, the desktop app or CI — and when the code changes, Outry shows which requests no longer match it.
curl -fsSL https://raw.githubusercontent.com/1rowvy/outry/master/install.sh | sh~/shop-api $ ✓ api/auth/login.outry Login POST 200 84ms ✓ api/users/create.outry CreateUser POST 201 41ms ✓ status == 201 ✓ body matches User ✓ api/orders/create.outry CreateOrder POST 201 57ms ✓ body.total == 25 ✓ api/orders/wait-paid.outry WaitUntilPaid GET 200 3 polls 2.1s ✓ api/flows/checkout.outry Checkout flow 5 requests 2.4s 5 passed, 0 failed · 1 login, cached 4× · 2.6s
01 / format
One file is the docs, the request and the test.
A .outry request reads like the HTTP it sends. Hover a part to see what it does.
// Creates an order for the current user.CreateOrder: POST /v1/orders { only: [dev, staging] headers { Authorization: "Bearer ${Login().body.token}" Idempotency-Key: uuid() } body { customer: CreateUser(name: "Ada").body.id, items: [{ sku: "BOOK-1", qty: 2 }], } expect { status == 201 body.total == 25 body matches Order } save order_id = body.id}
- 01Name
Requests are named like functions and unique per project. No imports.
- 02Environments
onlylimits where a request may run — directly or through a call. - 03Calls
Login()is a request. Called ten times, sent once per run. - 04Body
JSON-like literals with expressions, functions like
uuid()and calls inside. - 05Checks
Every line is a boolean. All run; failures print both sides.
- 06Shapes
matches Ordervalidates structure — named shapes or JSON Schema. - 07Save
Typed values for later requests, persisted outside the repo.
02 / sync
It notices when the code moves.
Outry parses your Go service with tree-sitter and compares every request with its handler: method, path, body fields and types, query, headers, middleware, response type. Drift fails CI on the exact line — with a fix.
- 1a struct field is renamed
- 2outry check
- 3--fix rewrites the request
package users type CreateUser struct { Name string `json:"name"`FullName string `json:"full_name"` Email string `json:"email"`} func (h *Handler) Create(c *gin.Context) { var in CreateUser if err := c.ShouldBindJSON(&in); err != nil { c.JSON(400, errBody(err)) return } c.JSON(201, h.svc.Create(in))}
CreateUser: POST /v1/users { handler: users.Create body { name: "Ada",full_name: "Ada", email: "ada@example.com", } expect { status == 201 body matches User }}
$ outry check api/v1/users/post.outry:5:5: error: required field `full_name` (string) is missing from body ← internal/api/users.go:4 api/v1/users/post.outry:5:5: error: body field `name` is not in users.CreateUser ← internal/api/users.go:4 9 files, 2 environments, 12 Go routes: 2 errors, 0 warnings; 2 fixable with `outry import go --fix`$ outry import go --fix ~ api/v1/users/post.outry POST /v1/users internal/api/users.go:8 fixed 2 differences in 1 file $ outry check 9 files, 2 environments, 12 Go routes: 0 errors, 0 warnings# git diff internal/api/users.go - Name string `json:"name"` + FullName string `json:"full_name"`
chi · gin · net/http (Go 1.22 patterns) · group prefixes · Mount across packages · middleware
03 / composition
Requests compose like functions.
A flow is a scenario. Calls resolve dependencies, share one cookie jar and one call cache, and every hop is traced with its response and timing.
// A user buys one item and pays.flow Checkout { user = CreateUser(name: "Ada") order = CreateOrder(customer: user.body.id) Pay(order: order.body.id) WaitUntilPaid(id: order.body.id) expect { GetOrder(id: order.body.id).body.status == "paid" } save last_order = order.body.id}
04 / architecture
One engine everywhere.
Parser, evaluator, HTTP, variables, secrets and Go import live in outry-core. Every surface is a thin wrapper, so what passes on your machine passes in CI.
- outry-coreRust · parser · eval · reqwest · tree-sitter
- CLIrun · check · fmt · import
- DesktopTauri 2 · auto-update
- VS CodeLSP + response view
- Any editorNeovim · Helix · Zed
- CIannotations · JSON Lines
05 / variables
Every value has exactly one origin.
Variables resolve through a fixed chain — and outry vars tells you which layer each value came from. Secrets stay in the system keychain or OUTRY_*.
- 1
--varoverride for one run - 2
savecaptured from responses - 3
OUTRY_<NAME>process environment, CI secrets - 4
env.toml [env.X] → [vars]committed with the code - 5
keyringsystem keychain, queried lazily
$ outry vars -e dev project shop-api · env dev base env.toml http://localhost:8080 token keychain eyJ…(212 chars) order_id saved o_81f page --var 2 api_key missing not set: outry secret set api_key --env dev
06 / CI
Your spec is your test suite.
Static checks first — syntax, call graph, variables per environment, formatting, Go drift — without sending a byte. Then run against the real service.
name: API testson: [push, pull_request] jobs: api: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: curl -fsSL https://raw.githubusercontent.com/1rowvy/outry/master/install.sh | sh - run: outry check --format github - run: outry run api --env ci --no-keyring --fail-fast env: OUTRY_TOKEN: ${{ secrets.API_TOKEN }}
error variable `api_key` is not defined in staging, prod
0 passed1 failed2 could not start07 / documentation
Read the manual.
Guides walk through real tasks; the reference covers every keyword, flag and config key.
Start here
- Getting startedInstall Outry, create a project and send your first request in a minute.
- Example projectA Gin service whose api/ folder uses every feature of Outry — run it, then change the code and watch the requests notice.
- Outry and OpenAPIOpenAPI describes what an API promises. Outry checks that the running service keeps that promise — and that your requests still match the code.
- InstallationInstall the Outry CLI and desktop app, keep them up to date.
Guides
- Calling requests and checksCheck responses with expect, call one request from another, describe scenarios with flow.
- Variables and environmentsenv.toml environments, shared variables, overrides and where each value comes from.
- SecretsKeep tokens and passwords in the system keychain locally and in environment variables in CI.
- Running Outry in CIUse your request files as API tests in GitHub Actions and other CI systems.
- Import routes from GoGenerate .outry requests for every chi, gin and net/http route of a Go service.
- Desktop appBrowse, edit and send requests in the Outry desktop app.
- EditorsEdit .outry files in VS Code, Neovim, Helix or any editor with an LSP client — errors, completion, go to definition and sending requests come from `outry lsp`.
- .http filesThe older .http request format — still supported — and converting it to .outry.
Reference
CLI at a glance
outry run <paths|names>- send requests, run checks
outry check- static analysis, envs, fmt, Go drift
outry fmt- canonical style, like gofmt
outry import go- generate & diff requests from routes
outry vars -e prod- every variable and its source
outry secret set token- store a secret in the keychain
outry lsp- language server for any editor
outry convert- .http → .outry
Put your API next to its code.
Linux, macOS and Windows. One static binary, no runtime.
curl -fsSL https://raw.githubusercontent.com/1rowvy/outry/master/install.sh | sh