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 — outry
~/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
0collections to export
1engine for CLI, app, editors, CI
3Go routers read: chi · gin · net/http
∞requests per run, one Login()

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.

api/orders/create.outry
// 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}
  1. 01
    Name

    Requests are named like functions and unique per project. No imports.

  2. 02
    Environments

    only limits where a request may run — directly or through a call.

  3. 03
    Calls

    Login() is a request. Called ten times, sent once per run.

  4. 04
    Body

    JSON-like literals with expressions, functions like uuid() and calls inside.

  5. 05
    Checks

    Every line is a boolean. All run; failures print both sides.

  6. 06
    Shapes

    matches Order validates structure — named shapes or JSON Schema.

  7. 07
    Save

    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.

  1. 1a struct field is renamed
  2. 2outry check
  3. 3--fix rewrites the request
internal/api/users.go
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))}
api/v1/users/post.outry
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.

api/flows/checkout.outry
// 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}
trace00.6s1.2s1.8s2.4s
✓Checkoutflow
2.4s
201CreateUserPOST /v1/users
120ms
200LoginPOST /auth/login
84ms
201CreateOrderPOST /v1/orders
160ms
200Logincached
0ms
202PayPOST /v1/orders/o_81f/pay
95ms
200WaitUntilPaidGET /v1/orders/o_81f
2.0s
200GetOrderGET /v1/orders/o_81f
55ms
poll every 1stotal 2.39s · 7 requests · 1 cookie jar

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-sitterCLIrun · check · fmt · importDesktopTauri 2 · auto-updateVS CodeLSP + response viewAny editorNeovim · Helix · ZedCIannotations · JSON Lines
  • 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. 1--varoverride for one run
  2. 2savecaptured from responses
  3. 3OUTRY_<NAME>process environment, CI secrets
  4. 4env.toml [env.X] → [vars]committed with the code
  5. 5keyringsystem 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.

.github/workflows/api.yml
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 }}
outry checksyntax · calls · 3 envs · fmt · 18 Go routes0.4s
outry run api --env ci42 requests · 6 flows · 118 checks9.8s
annotationapi/orders/create.outry#L7PR

error variable `api_key` is not defined in staging, prod

0 passed1 failed2 could not start

07 / documentation

Read the manual.

Guides walk through real tasks; the reference covers every keyword, flag and config key.

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