tat chains dependent HTTP requests โ authenticate, capture values, reuse them in the next call โ so you verify whole integration flows in JSON or YAML, from the CLI.
Write tests in .tat.json or .tat.yml files. Human-readable, version-controllable, no code required.
Assert status codes, fields, strings, ranges, null checks, and response times with expressive operators.
Extract values from responses and use them in subsequent requests. For isolated test runs, pass missing values explicitly with --variables.
Tag suites, filter by tag, or run specific suites and individual tests. Run only what matters.
Static tokens, OAuth via setup scripts, or --env-cmd for CI. Interactive 2FA prompts supported.
Console, JSON, or JUnit XML. Integrate with GitHub Actions, Jenkins, and any CI system.
Use the tat-create skill to turn API specs, OpenAPI docs, and endpoint notes into runnable tat files faster.
Run tat suites from Test Explorer, use CodeLens in the editor, and get prompted for capture-derived values when running a single test.
Most tools push you to test endpoints in isolation. But real integration work is a sequence of dependent steps — data from one response flows into the next request, and the whole chain represents one business scenario.
tat is designed around that shape. Define a flow as an ordered list of tests, capture values from each response, and feed them straight into the next call. One file, one command, the whole journey verified.
Verify the flow first, then build with confidence.
setup or login requestid from the response{{workspaceId}}Install the CLI + the agent skill
Works with Claude Code, Copilot CLI, Gemini CLI, and more
That's it. Your agent writes the tests, you run them.
Open the sample API project in StackBlitz, run tat against a live sample API, and see a real flow pass in seconds.
A test file has a simple structure: environment variables, optional setup, and an array of test suites.
{
"$schema": "https://unpkg.com/@nanotiny/tiny-api-test/schema.json",
"env": { "baseUrl": "https://api.example.com" },
"setup": "node scripts/get-token.js",
"timeout": 5000,
"suites": [ ... ]
}
env:
baseUrl: https://api.example.com
setup: node scripts/get-token.js
timeout: 5000
suites:
- ...
| Field | Type | Required | Description |
|---|---|---|---|
$schema | string | No | JSON schema URL for editor autocomplete |
env | object | string | No | Inline env variables or path to a JSON file |
setup | string | No | Shell command run before tests; JSON stdout merged into env |
timeout | number | No | Default request timeout in ms for all tests |
suites | Suite[] | Yes | List of test suites |
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Suite name |
tags | string[] | No | Tags used to filter suites via --tag |
skip | boolean | No | Skip all tests in this suite |
tests | Test[] | Yes | List of tests |
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Test name |
method | string | Yes | HTTP method: GET, POST, PUT, PATCH, DELETE, HEAD |
url | string | Yes | Request URL, supports {{variable}} interpolation |
headers | object | No | HTTP headers as key-value pairs |
body | any | No | Request body, objects auto-serialized as JSON |
assert | string[] | No | Assertion expressions evaluated against the response |
capture | object | No | Extract response values for use in later tests |
response | true | object | No | Include response status, body, and/or headers in output |
skip | boolean | No | Skip this test without failing |
timeout | number | No | Per-test timeout in ms (highest priority) |
Each assertion is evaluated against the response context. Body fields from object responses are spread at the root for easy access.
// Response: { status: 200, body: { id: 1, name: "Alice" } }
// Available in assertions:
$status = 200 // HTTP status code
$headers = { ... } // Response headers
$body = { id: 1, ... } // Full response body
$duration = 142 // Request time in ms
id = 1 // Body fields spread at root
name = "Alice" // Body fields spread at root
| Operator | Example | Description |
|---|---|---|
== != | name == 'Alice' | Equals / not equals |
> < >= <= | age >= 18 | Numeric comparison |
is null | deletedAt is null | Value is null or missing |
is not null | id is not null | Value exists |
contains | bio contains 'engineer' | String contains substring |
startswith | name startswith 'Ali' | String starts with |
endswith | email endswith '.com' | String ends with |
like | name like 'Ali*' | Wildcard match (* = any) |
in / not in | role in 'admin,user' | Value in comma-separated list |
between | age between 18 and 65 | Value in range (inclusive) |
&& / || | status == 'ok' && code == 0 | Logical AND / OR |
Use $duration to assert response time:
"assert": ["$status == 200", "$duration < 500"]
assert:
- "$status == 200"
- "$duration < 500"
When an assertion fails, tat prints the referenced actual values on the same line so you can see what the API returned without rerunning in a debugger.
$ tat run tests.tat.yml
Users
โ Get user (86ms)
โ $status == 200 (actual: $status=500)
โ name == "Alice" (actual: name="Bob")
Use response on a test when you want to print response details in the test output. response: true keeps the compact shortcut for body and headers; use the object form to request status, body, and headers individually.
response: true includes body and headers.response: { status: true } includes only the response status code.response: { body: true } includes only the response body.response: { headers: true } includes only the response headers.{
"name": "Inspect status",
"method": "GET",
"url": "{{baseUrl}}/users/1",
"response": { "status": true },
"assert": ["$status == 200"]
}
- name: Inspect status
method: GET
url: "{{baseUrl}}/users/1"
response:
status: true
assert:
- "$status == 200"
Users
โ Inspect status (72ms)
Response status: 200
Use capture to extract values from a response and reuse them in later tests with {{variable}} interpolation.
// Create a user and capture the ID
{
"name": "Create user",
"method": "POST",
"url": "{{baseUrl}}/users",
"body": { "name": "Alice" },
"assert": ["$status == 201"],
"capture": { "userId": "id" }
}
// Use the captured ID in a later test
{
"name": "Get user",
"method": "GET",
"url": "{{baseUrl}}/users/{{userId}}",
"assert": ["$status == 200", "name == 'Alice'"]
}
# Create a user and capture the ID
- name: Create user
method: POST
url: "{{baseUrl}}/users"
body:
name: Alice
assert:
- "$status == 201"
capture:
userId: id
# Use the captured ID in a later test
- name: Get user
method: GET
url: "{{baseUrl}}/users/{{userId}}"
assert:
- "$status == 200"
- "name == 'Alice'"
Capture uses @nanotiny/json-expression query syntax โ filter arrays, use indexes, traverse nested paths:
"capture": {
"companyName": "companies.name | id == 'c2'", // filter by field
"thirdId": "companies.id | $index == 2", // by array index
"firstType": "types.type.name", // nested path
"hpStat": "stats.base_stat | $index == 0" // indexed nested
}
capture:
companyName: "companies.name | id == 'c2'" # filter by field
thirdId: "companies.id | $index == 2" # by array index
firstType: types.type.name # nested path
hpStat: "stats.base_stat | $index == 0" # indexed nested
Define variables in the env field (inline or external file). Variables are interpolated in url, headers, and body:
{
"env": { "baseUrl": "https://api.example.com", "token": "abc123" },
// or load from file:
"env": "./env.local.json"
}
env:
baseUrl: https://api.example.com
token: abc123
# or load from file:
env: ./env.local.json
When you run a single test with --suite and --test, tat keeps that run isolated. Earlier tests are not executed automatically, so capture-derived values must come from env, setup, --env-cmd, or manual --variables input.
$ tat run tests.tat.yml --suite "Workspace flow" --test "Create project" --variables workspaceId=ws-123
Run tests from a JSON/YAML file, or from a directory (discovers all .tat.json, .tat.yml, .tat.yaml files recursively).
| Option | Description |
|---|---|
--tag <tags> | Filter suites by tag (comma-separated, OR logic) |
--suite <name> | Run a single suite by name |
--test <name> | Run a single test by name (requires --suite) |
--variables <key=value> | Supply a manual variable value for the run; repeat to pass more than one |
--output <format> | console (default), json, or junit |
--out <file> | Write output to a file |
--bail | Stop on first failure |
--env-cmd <cmd> | Run command; JSON stdout merged into env |
--timeout <ms> | Global request timeout in ms |
--insecure | Disable TLS certificate verification for trusted non-local development endpoints; localhost HTTPS is handled automatically |
Check syntax, schema validity, and warn about undefined {{variables}} without running tests.
$ tat validate tests.tat.json
tests.tat.json: valid
$ tat validate tests.tat.json
tests.tat.json: valid (1 warning)
[warn] test "Get user": variable "{{token}}" is not defined
| Code | Meaning |
|---|---|
0 | All tests passed |
1 | One or more tests failed |
2 | Configuration error (bad JSON/YAML, schema error, missing file) |
Highest to lowest: per-test timeout field → --timeout CLI flag → file-level timeout → no timeout.
For APIs where you already have a token โ put it in your env:
{
"env": { "token": "eyJhbG..." }
}
env:
token: eyJhbG...
The setup command runs before tests. Its JSON stdout merges into env. Interactive prompts work because stdin/stderr are inherited:
{
"setup": "node scripts/get-token.js",
"suites": [...]
}
setup: node scripts/get-token.js
suites:
- ...
// scripts/get-token.js โ print JSON to stdout
// ... do OAuth flow, prompt for 2FA ...
console.log(JSON.stringify({ token: access_token }));
Same as setup but from the command line โ useful when auth differs per environment:
$ tat run tests.json --env-cmd "node scripts/ci-token.js"
# Or inline for simple cases:
$ tat run tests.json --env-cmd \
"node -e \"console.log(JSON.stringify({token:process.env.API_TOKEN}))\""
| Scenario | Approach |
|---|---|
| Local dev, token known | Paste into env.local.json (gitignored) |
| Local dev, OAuth + 2FA | setup script with interactive prompt |
| CI, OAuth client credentials | --env-cmd with non-interactive script |
| CI, long-lived API key | Inject via environment variable or --env-cmd |
Use JUnit output for CI systems. Here's a GitHub Actions example:
name: API Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 18
- run: npm i -g @nanotiny/tiny-api-test@latest
- run: tat run tests.tat.json --output junit --out results.xml
- uses: actions/upload-artifact@v4
with:
name: test-results
path: results.xml
Get Test Explorer integration, CodeLens, editor support, and prompts for missing capture-derived values during isolated single-test runs:
$ code --install-extension nanotiny.tat-test-runner
tat is agent-friendly. Install the tat-create skill to let AI coding assistants like Claude Code, Copilot CLI, or Gemini CLI generate .tat.json and .tat.yml files from your API specs, OpenAPI/Swagger docs, endpoint descriptions, or request/response examples:
$ npx skills add https://github.com/nanotinydev/tat-skills
Once installed, prompt your assistant with something like:
# Example prompt for your AI assistant:
Use tat-create to generate a tat test file from this API specification.
Include login, get user profile, and create order flows.
Use YAML format and add assertions for status codes and key response fields.
If authentication is required, create a setup hook and reuse the token.
The skill handles suites, requests, assertions, captures, and authentication flows โ turning your API spec into runnable tests in seconds.