restbook
Keep your API requests in version-controlled .http files and run them from the terminal and CI. restbook understands the format popularised by IDE REST clients and adds what CI needs: environments, request chaining (use the token from login in later requests), a small safe assertion language, concurrency, curl/HAR conversion and JUnit/JSON/Markdown reports with meaningful exit codes.
Features
- Parser for
.http/.rest:###separators with names,# @name,#and//comments, file variables (@baseUrl = ...), raw/JSON/form-urlencoded/multipart bodies,< ./fileincludes, GraphQL (X-REQUEST-TYPE: GraphQL), directives@no-redirect,@timeout 5s,@skip,@tag,@after. Parse errors carry exact line numbers. - Variables and environments:
{{var}},http-client.env.jsonwith named envs,$sharedand a merged private env file,{{$env.TOKEN}},{{$uuid}},{{$timestamp}},{{$randomInt 1 100}},{{$datetime iso8601 +1d}}. - Chaining:
{{login.response.body.$.token}},{{login.response.headers.Location}}, with automatic dependency ordering and cycle detection. - Assertions:
?? status == 200,?? body.$.items.length > 0,?? header content-type contains json,?? duration < 500,?? body matches /regex/,?? body schema ./schemas/user.json. No embedded scripting. - Execution (httpx): cookie jar per run, redirect policy, TLS verify toggle, proxy, retries, timeouts, concurrency for independent requests (
-j), selection by name/tag/glob (--only),--env, dry run with secrets masked. - Interop:
restbook curl file.http nameexports curl;import-curlandimport-harproduce.http. - Output: coloured terminal view with truncated, highlighted JSON; responses saved to
.restbook/responses/<name>.json; JUnit XML, JSON and Markdown reports; non-zero exit on failure.
Install
cd restbook
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate # provides the `restbook` command
Requires Python 3.12+.
Quickstart
# terminal 1: the bundled demo API
python examples/api/server.py 8184
# terminal 2
cd examples/requests
restbook run auth.http notes.http features.http --env dev -j 4
Usage
Writing a request file
@contentType = application/json
### Login
# @name login
POST {{baseUrl}}/login
Content-Type: {{contentType}}
{"username": "{{username}}", "password": "{{password}}"}
?? status == 200
?? body.$.token matches /^[0-9a-f]{32}$/
### Who am I
# @name me
GET {{baseUrl}}/me
Authorization: Bearer {{login.response.body.$.token}}
?? status == 200
?? body.$.name == {{username}}
Reference: docs/file-format.md, docs/assertions.md.
Running
$ restbook run auth.http --env dev --only login,me
PASS login POST http://127.0.0.1:8184/login 200 2 ms 101 B
ok status == 200
ok body.$.token matches /^[0-9a-f]{32}$/
ok body.$.user.roles.length == 2
ok body.$.user.roles[0] == "admin"
ok header set-cookie contains session=
{
"token": "7f036c0481fec956a3aa7d6ada59e5d5",
"user": {
"name": "demo",
"roles": [
"admin",
"editor"
]
}
}
PASS me GET http://127.0.0.1:8184/me 200 1 ms 16 B
ok status == 200
ok body.$.name == demo
{
"name": "demo"
}
2 passed, 0 failed, 0 skipped in 2 ms
--only accepts comma-separated names, tags and globs (--only 'login,users*,crud'). Requests that a selected request depends on are added automatically.
All three example files, four at a time:
$ restbook run auth.http notes.http features.http --env dev --body-lines 0 -j 4 --no-save | tail -3
ok duration < 3000
24 passed, 0 failed, 1 skipped in 433 ms
Failures and exit codes
$ restbook run health.http --no-save
FAIL h GET http://127.0.0.1:8184/health 200 1 ms 16 B
fail status == 201 (line 4) (got 200)
ok body.$.status == "ok"
...
0 passed, 1 failed, 0 skipped in 1 ms
$ echo $?
1
| Exit code | Meaning |
|---|---|
| 0 | every request passed (skipped ones do not count as failures) |
| 1 | at least one failed assertion or request error (connection, timeout, unresolved reference) |
| 2 | usage, parse, environment or dependency-cycle error; nothing was sent |
Parse and dependency errors point at the problem:
$ restbook run bad.http
parse error: bad.http:2: invalid header line 'bad header' (expected 'Name: value'; a blank line must precede the body)
$ restbook run cy.http
error: dependency cycle: a -> b -> a
Dry run (secrets masked)
$ restbook run notes.http --env dev --only create --dry-run
### login
POST http://127.0.0.1:8184/login
Content-Type: application/json
{"username": "demo", "password": "****"}
### create
POST http://127.0.0.1:8184/notes
Authorization: ****
Content-Type: application/json
{
"title": "Release checklist 990",
"body": "created at 2026-10-16T16:22:17.602Z, due 2026-10-17T16:22:17.602Z",
"tags": ["ops", "7fce37dd-fa58-46cd-8231-4796198b7c68"]
}
curl export and import
$ restbook curl notes.http login --env dev --mask
curl -X POST http://127.0.0.1:8184/login -L -H 'Content-Type: application/json' --data-raw '{"username": "demo", "password": "****"}'
$ restbook import-curl "curl -X POST https://api.example.com/v1/items -H 'Content-Type: application/json' -H 'Authorization: Bearer abc' -d '{\"name\":\"x\"}' -L"
### post-v1-items
POST https://api.example.com/v1/items
Content-Type: application/json
Authorization: Bearer abc
{"name":"x"}
$ restbook import-har capture.har -o requests.http
Responses to earlier requests cannot be known without sending, so restbook curl leaves {{login.response...}} placeholders in place.
Reports
restbook run notes.http --env dev --junit report.xml --json report.json --markdown report.md
Responses are saved as .restbook/responses/<name>.json (change with --out-dir, disable with --no-save), containing the masked request and the full response with parsed JSON body.
run options
| Option | Purpose |
|---|---|
--env NAME |
environment from http-client.env.json (+ private file) |
--var K=V |
override a variable (beats file and env values) |
--only LIST |
names, tags or globs |
-j N |
run independent requests concurrently |
--dry-run |
print resolved requests, send nothing |
--insecure, --proxy URL |
TLS verification off, proxy |
--retries N |
retry transport errors and 502/503/504 with backoff |
--timeout S, --no-redirect |
defaults; per-request @timeout / @no-redirect win |
--junit F, --json F, --markdown F |
reports |
--body-lines N |
response lines shown (0 hides bodies) |
Configuration
There is no global config file. Behaviour comes from the command line, the .http file and the environment files described in docs/file-format.md. The example http-client.private.env.json contains only the demo API's password; real projects should git-ignore it.
Architecture
Module (src/restbook/) |
Role |
|---|---|
parser.py |
.http text to ParsedFile/Request, line-accurate errors |
scope.py |
variable resolution, response references, dependency graph, cycle detection, topological order |
dynamic.py |
$uuid, $timestamp, $randomInt, $datetime |
jsonpath.py |
JSONPath subset |
assertions.py |
assertion DSL parse and evaluation, JSON Schema check |
build.py |
resolved request: bodies, multipart, GraphQL, file includes |
envfiles.py, masking.py |
environment files; secret masking |
runner.py |
selection, httpx execution, retries, concurrency |
interop.py |
curl export/import, HAR import, .http rendering |
report.py |
JUnit, JSON, Markdown, saved responses |
cli.py |
click commands |
Design notes with worked examples: docs/design.md. Demo API: examples/api/server.py (stdlib: login/token, notes CRUD, pagination, slow endpoint, redirect, cookies, form, multipart upload, GraphQL echo).
Tests
.venv/bin/pytest -q
Covers the parser (including error line numbers), variables, dynamic values, dependency ordering and cycles, JSONPath, assertions and schemas, multipart/file includes, curl/HAR round trips, end-to-end runs against the demo API (sequential and concurrent), JUnit validity and CLI exit codes.
Limitations
- No scripting (
> {% %}blocks); use the assertion DSL, or--varplus response references. - JSONPath is a subset: no filters (
[?()]) or slices. - Without
-j, requests run in dependency order and otherwise in file order. With-j, unrelated requests may overlap; use# @afterfor order-sensitive sequences. - Response references are scoped to one file; they cannot cross files.
- Assertions on
durationmeasure a single attempt, not retries. - HTTP/2, client certificates and streaming bodies are not supported.