An API-as-code workbench for the terminal.
Resterm is an open source API client that keeps requests in plain .http files next to your code. Use the keyboard-driven TUI, or run the same files in CI with resterm run.
HTTPGraphQLgRPCWebSocketSSEmacOS, Linux, WindowsApache-2.0
###Take the tour
Click the screen and press the keys shown under it, or use the buttons.
Requests and their mocks live in one .http file.
The mock server answers from the same file.
Run the selected request. Assertions check the response.
@trace times every phase and checks it against the budget.
Explain shows how the request was built: variables, settings and the final request.
Select the onboard workflow and run it. Each step reuses values captured by the last.
Everything is on the keyboard. Help is searchable and works offline.
Pick a theme, or write your own in TOML.
###Requests are plain text
Requests, mocks, workflows, environments: almost everything in Resterm is a file in your repo. Use your usual editor, or Resterm's terminal UI if you want autocomplete. Changes show up in git diff just like any other code. Below, requests.http holds both the requests and their mocks, so no real backend is needed.
### Mock: say hello
# @mock method=POST path=/hello
# @match json={"kind":"greeting"}
HTTP/1.1 200 OK
Content-Type: application/json
{"message":"hello from the mock","name":{{json.body.name}}}
### Mock: create a member
# @mock method=POST path=/users
# @match headers={"Authorization":{"prefix":"Bearer "}}
# @match json={"role":"member"}
HTTP/1.1 201 Created
Content-Type: application/json
{"id":{{json.body.id}},"name":{{json.body.name}}}
### Say hello
# @name Hello
# @assert response.statusCode == 200
# @assert response.json("name") == "Resterm"
POST {{base.url}}/hello
Content-Type: application/json
{"kind":"greeting","name":"Resterm"}
### Create users from a list of names
# @name CreateUsers
# @for-each ["ada","linus","grace"] as name
# @auth bearer {{auth.token}}
# @assert response.statusCode == 201
# @assert response.json("name") == name
POST {{base.url}}/users
Content-Type: application/json
{"id":"user-{{= name }}","name":"{{= name }}","role":"member"}$ resterm mock requests.http &
Mock server listening on http://127.0.0.1:8080 (2 routes, 2 scenarios)
$ resterm run --env dev --all requests.http
Running 2 target(s) from requests.http with env dev
PASS POST Hello [200 OK in 12.5625ms]
Source Target: {{base.url}}/hello
Effective Target: http://127.0.0.1:8080/hello
PASS FOR-EACH POST CreateUsers [3 passed, 0 failed, 0 skipped in 2.807625ms]
1. PASS POST CreateUsers (1/3) [201 Created in 958.5µs]
2. PASS POST CreateUsers (2/3) [201 Created in 789.708µs]
3. PASS POST CreateUsers (3/3) [201 Created in 928.375µs]
Summary: total=2 passed=2 failed=0 skipped=0$ resterm mock requests.http &Mock server listening on http://127.0.0.1:8080 (2 routes, 2 scenarios)$ resterm run --env dev --all requests.httpRunning 2 target(s) from requests.http with env devPASS POST Hello [200 OK in 12.5625ms] Source Target: {{base.url}}/hello Effective Target: http://127.0.0.1:8080/helloPASS FOR-EACH POST CreateUsers [3 passed, 0 failed, 0 skipped in 2.807625ms] 1. PASS POST CreateUsers (1/3) [201 Created in 958.5µs] 2. PASS POST CreateUsers (2/3) [201 Created in 789.708µs] 3. PASS POST CreateUsers (3/3) [201 Created in 928.375µs]Summary: total=2 passed=2 failed=0 skipped=0$
###What it does
Pick a feature to see how it looks in a request file.
•▾features.http (10)
# Requests are plain text in .http or .rest files.
# The # @ lines on top name the request, capture the token and check the status.
### Log in
# @name login
# @capture file auth.token {{response.json.token}}
# @assert response.statusCode == 200
POST {{base.url}}/login
Content-Type: application/json
{"user": "ada", "password": "{{password}}"}# GraphQL, gRPC, WebSocket and SSE requests go in the file next to your HTTP ones.
### Generate report
# @name GenerateReport
# @grpc analytics.ReportingService/GenerateReport
# @grpc-reflection true
# @grpc-metadata x-trace-id: {{$uuid}}
GRPC {{grpc.host}}
{"reportId": "rep-42", "includeHistorical": true}# Run requests as steps and pass values from one to the next.
# @workflow signup
# @step Create using=CreateUser expect.statuscode=201
# @if last.json().plan != "team" fail="expected a team account"
# @for-each ["admin", "billing"] as role
# @step Grant using=GrantRole
### Create user
# @name CreateUser
# @capture file user.id {{response.json.id}}
POST {{base.url}}/users
### Grant role
# @name GrantRole
PUT {{base.url}}/users/{{user.id}}/roles/{{= role }}# Mocks go in your .http files, next to the requests that call them.
# The first poll gets 503 pending. Every poll after that gets 200 completed.
### Poll payment
# @mock method=GET path=/payments/{id} sequence=polling
HTTP/1.1 503 Service Unavailable
Retry-After: 1
{"status": "pending"}
---
HTTP/1.1 200 OK
{"status": "completed"}# Proxy your app through Resterm and save the traffic as requests, mocks or both.
# resterm record --upstream https://api.example.com --out captured.http --mode both
### get-users-42
# @name get-users-42
# @assert response.statusCode == 200
GET {{recordedBaseUrl}}/users/42
Accept: application/json
### get-users-42-mock
# @mock method=GET path=/users/42 name=get-users-42-mock interpolate=false
HTTP/1.1 200 OK
{"id": 42, "name": "Ada"}# OAuth 2.0 with PKCE, or reuse a token from a CLI you're logged in to, like gh.
# @auth file command cmd="gh auth token"
### GitHub user
GET https://api.github.com/user
Accept: application/vnd.github+json
### Sign in with OAuth 2.0 and PKCE
# @auth oauth2 grant=authorization_code auth_url={{oauth.authUrl}} token_url={{oauth.tokenUrl}} client_id={{oauth.clientId}}
GET {{api.url}}/me# No ssh -L or kubectl port-forward first. Resterm opens the tunnel for the request.
### Through a bastion
# @ssh request host=bastion.example.com user=ops key=~/.ssh/id_ed25519
GET http://10.0.0.10/v1/health
### Through a Kubernetes port-forward
# @k8s namespace=default service=api port=http
GET http://api.default.svc.cluster.local/v1/health# Trace one call against budgets, profile 50 runs, or compare it across environments.
### Trace with budgets
# @trace dns<=40ms connect<=120ms ttfb<=300ms total<=600ms
GET {{base.url}}/health
### Profile latency
# @profile count=50 warmup=5
GET {{base.url}}/health
### Compare environments
# @compare dev stage prod base=stage
GET {{base.url}}/health# A small language for request logic, and you can write your tests in it too.
### Search
# @skip-if env.mode == "dry-run"
# @assert len(response.json("items")) > 0
# @rts test "every item has an id"
> {%
for let i, it range response.json("items") {
if it.id == null {
t.error("item", i, "has no id")
}
}
%}
# @rts pre-request
> request.setHeader("X-Request-ID", uuid())
GET {{base.url}}/search?q={{= url.encode(vars.get("query")) }}
Authorization: {{= "Bearer " + vars.get("auth.token") }}# In CI, run the file headless. This picks the smoke requests and writes JUnit XML.
# resterm run --tag smoke --format junit api.http > report.xml
### Health
# @name health
# @tag smoke
# @assert response.statusCode == 200
# @assert response.json("status") == "ok"
GET {{base.url}}/health###Install
A single binary for macOS, Linux and Windows.
$ brew install restermmacOS and Linux
$ curl -fsSL https://raw.githubusercontent.com/unkn0wn-root/resterm/main/install.sh | bashmacOS and Linux
$ iwr -useb https://raw.githubusercontent.com/unkn0wn-root/resterm/main/install.ps1 | iexPowerShell
$ go install github.com/unkn0wn-root/resterm/cmd/resterm@latestGo 1.25 or newer
Release binaries for macOS, Linux and Windows are on the releases page. Installed without Homebrew? resterm --update updates in place.
$ mkdir my-api && cd my-api$ resterm init$ resterm
resterm init writes a small project with local mocks. Press g Shift+M to start the mock server, then Ctrl+Enter to send the request under the cursor.
- Status
- 200 OK
- Telemetry
- none
- Account
- none
- License
- Apache-2.0
Free and open source. Your requests stay plain text in your repo.