Bruno
7 min read
Open-source API client (Postman/Insomnia alternative). Collections are stored as plain-text .bru files on disk — fully version-control friendly, no mandatory cloud sync or account required.
Supports REST, GraphQL, gRPC, SOAP, and WebSocket requests.
Key Differences from Postman
• Collections live in a folder on disk, not the cloud
• .bru format is human-readable, diffable, and grep-able
• No account required
• CLI (@usebruno/cli) for CI/CD
• Auth can be inherited from collection → folder → request
Collection Structure
my-api/
├── bruno.json # collection config (name, version)
├── .env # process env vars (gitignored)
├── environments/
│ ├── local.bru
│ └── production.bru
├── users/
│ ├── folder.bru # folder-level vars, auth, scripts
│ ├── get-users.bru
│ └── create-user.bru
└── auth/
└── login.bru
.bru File Format
Full request example
meta {
name: Create User
type: http
seq: 2
tags: users, write
}
post {
url: {{baseUrl}}/users
body: json
auth: bearer
}
params:query {
version: v2
}
params:path {
~id: 123
}
headers {
Accept: application/json
X-Request-ID: {{$uuid}}
}
auth:bearer {
token: {{token}}
}
body:json {
{
"name": "Angelo",
"email": "angelo@example.com"
}
}
script:pre-request {
bru.setVar("timestamp", Date.now());
}
script:post-response {
bru.setEnvVar("userId", res.body.id);
}
assert {
res.status: equals 201
res.body.name: equals Angelo
res.responseTime: lt 500
}
tests {
test("user created", function() {
expect(res.getStatus()).to.equal(201);
expect(res.getBody().name).to.equal("Angelo");
});
}
docs {
Creates a new user. Requires Bearer auth.
}
Body types
| Block | Content-Type |
|---|---|
body:json | application/json |
body:text | text/plain |
body:xml | application/xml |
body:form-urlencoded | application/x-www-form-urlencoded |
body:multipart-form | multipart/form-data |
body:graphql | GraphQL query |
body:graphql:vars | GraphQL variables (JSON) |
Auth blocks
# Bearer
auth:bearer {
token: {{token}}
}
# Basic
auth:basic {
username: {{user}}
password: {{pass}}
}
# API Key (in header)
auth:apikey {
key: X-API-Key
value: {{apiKey}}
placement: header
}
# OAuth2 Client Credentials
auth:oauth2 {
grant_type: client_credentials
client_id: {{clientId}}
client_secret: {{clientSecret}}
token_url: {{tokenUrl}}
scope: read write
token_placement: header
token_prefix: Bearer
auto_fetch_token: true
}
# AWS Signature v4
auth:awsv4 {
accessKeyId: {{AWS_KEY}}
secretAccessKey: {{AWS_SECRET}}
region: us-east-1
service: execute-api
}
# Digest
auth:digest {
username: {{user}}
password: {{pass}}
}
Set auth: inherit on a request to use the parent folder's or collection's auth configuration.
Supported auth types
none, inherit, basic, bearer, digest, oauth2, awsv4, apikey, ntlm, wsse
Environment Files
Stored in environments/<name>.bru:
vars {
baseUrl: http://localhost:3000
version: v1
}
vars:secret {
token
apiKey
}
Secret variable names are listed without values — actual values are stored outside version control (in the app's encrypted store or via .env).
Variables
Types and precedence (highest → lowest)
| Type | Scope | Storage |
|---|---|---|
| Runtime | In-memory, current run | App memory |
| Request | Single request | .bru file |
| Folder | All requests in folder | folder.bru |
| Environment | Active environment | environments/*.bru |
| Collection | Entire collection | collection.bru |
| Global | All collections | App memory |
| Process env | System / .env file | .env file |
| Prompt | User input at runtime | Never stored |
Syntax in URLs, headers, body
{{baseUrl}}/users # any variable
{{process.env.TOKEN}} # process environment
{{?Enter token}} # prompt variable (asks user at runtime)
{{$uuid}} # dynamic variable
{{$randomFirstName}} # dynamic variable
{{$timestamp}} # dynamic variable
Dynamic variables
$uuid, $timestamp, $timestampMs, $randomInt, $randomFirstName, $randomLastName, $randomEmail, $randomPhoneNumber, $randomUrl, $randomIp, $randomAlphaNumeric, $randomBoolean
Assertions (assert block)
Declarative assertions evaluated after each response — no JavaScript needed:
assert {
res.status: equals 200
res.body.id: isDefined
res.body.name: equals Angelo
res.body.items: isArray
res.body.count: gt 0
res.body.count: lte 100
res.headers['content-type']: contains application/json
res.responseTime: lt 1000
}
Operators
| Category | Operators |
|---|---|
| Equality | equals, notEquals |
| Numeric | gt, gte, lt, lte, between |
| String | contains, notContains, startsWith, endsWith, matches, notMatches |
| Existence | isDefined, isUndefined, isNull |
| Type | isNumber, isString, isBoolean, isArray, isJson |
| Emptiness | isEmpty, isNotEmpty, isTruthy, isFalsy |
| Set | in, notIn |
| Length | length |
Access nested values with dot notation: res.body.user.address.city, res.body.items[0].price.
Scripting
JavaScript executed before/after requests. Uses CommonJS require().
req object
js
// URL
req.getUrl();
req.setUrl(url);
req.getHost();
req.getPath();
req.getQueryString();
req.getPathParams();
// Method
req.getMethod();
req.setMethod("POST");
req.getName();
req.getTags();
req.getAuthMode();
// Headers
req.getHeader("Authorization");
req.setHeader("X-Trace-Id", "abc");
req.getHeaders();
req.setHeaders({ "Content-Type": "application/json" });
req.deleteHeader("X-Unused");
// Body
req.getBody();
req.setBody({ key: "value" });
// Config
req.setTimeout(5000);
req.getTimeout();
req.setMaxRedirects(3);
req.getExecutionMode(); // "runner" | "standalone"
req.getExecutionPlatform(); // "app" | "cli"
res object
js
// Status
res.status; // 200
res.getStatus();
res.statusText;
res.getStatusText();
res.responseTime;
res.getResponseTime(); // ms
res.url;
res.getUrl();
// Headers
res.headers;
res.getHeaders();
res.getHeader("content-type");
// Body
res.body; // parsed JSON or raw string
res.getBody();
res.setBody(newBody); // replace for downstream tests
// Size
res.getSize(); // { body, headers, total } in bytes
bru object
js
// Active environment
bru.getEnvName();
bru.getEnvVar("baseUrl");
bru.setEnvVar("token", res.body.token);
bru.setEnvVar("token", val, { persist: true }); // survive restarts
bru.hasEnvVar("token");
bru.deleteEnvVar("token");
// Runtime variables (in-memory, current run)
bru.getVar("temp");
bru.setVar("temp", value);
bru.hasVar("temp");
bru.deleteVar("temp");
bru.deleteAllVars();
bru.getAllVars();
// Collection / folder / request vars (read-only)
bru.getCollectionVar("name");
bru.getFolderVar("name");
bru.getRequestVar("name");
bru.getCollectionName();
// Global vars
bru.getGlobalEnvVar("key");
bru.setGlobalEnvVar("key", value);
bru.getAllGlobalEnvVars();
// Secrets
bru.getSecretVar("API_KEY");
bru.getProcessEnv("HOME"); // process.env equivalent
bru.getOauth2CredentialVar("access_token");
bru.resetOauth2Credential(credentialId);
// Utilities
bru.interpolate("Hello {{name}}"); // resolve variables in a string
bru.cwd(); // collection root path
bru.sleep(500); // pause ms (async)
bru.isSafeMode(); // sandbox mode check
bru.disableParsingResponseJson(); // keep body as raw string
// Send HTTP requests from scripts
bru.sendRequest(
{
method: "GET",
url: "https://api.example.com/token",
headers: { Authorization: "Basic abc" },
timeout: 3000,
},
(err, res) => {
bru.setEnvVar("token", res.body.access_token);
},
);
bru.runRequest("auth/login"); // run another .bru request, returns response
// Runner control (collection runs only)
bru.runner.setNextRequest("users/get-users"); // branch to request
bru.runner.setNextRequest(null); // stop after this
bru.runner.skipRequest(); // skip current
bru.runner.stopExecution(); // halt entire run
// Test results
bru.getTestResults(); // [{name, status}]
bru.getAssertionResults(); // [{expression, operator, value, status}]
js
bru.cookies.get("session");
bru.cookies.has("session");
bru.cookies.all();
bru.cookies.toObject();
await bru.cookies.add({ name: "session", value: "abc", domain: "example.com" });
await bru.cookies.remove("session");
await bru.cookies.clear();
// Cookie jar (explicit URL context)
const jar = bru.cookies.jar();
jar.setCookie("https://example.com", "session", "abc");
jar.getCookies("https://example.com");
Inbuilt Libraries
Available in all sandbox modes (no --sandbox=developer needed):
| Library | Purpose |
|---|---|
axios | HTTP client for making additional requests |
chai | BDD/TDD assertion library |
moment | Date/time parsing and formatting |
uuid | RFC4122 UUID generation |
nanoid | Tiny unique string ID generator |
crypto-js | Crypto standards (AES, HMAC, SHA, MD5, Base64…) |
jsonwebtoken | Create and verify JWTs |
tv4 | JSON Schema v4 validator |
atob / btoa | Base64 encode/decode |
Developer Mode only (--sandbox=developer):
| Library | Purpose |
|---|---|
node-fetch | Fetch API for Node.js |
lodash | Utility functions |
ajv | JSON Schema validator (drafts 04–2020) |
cheerio | HTML/XML parsing |
xml2js | XML → JavaScript object |
Use in scripts with CommonJS:
js
const { sha256 } = require("crypto-js");
const jwt = require("jsonwebtoken");
const _ = require("lodash");
Request Chaining
Set variables from one response for use in the next request:
js
// script:post-response in login.bru
bru.setEnvVar("token", res.body.access_token);
bru.setEnvVar("userId", res.body.user.id);
Control flow in collection runs:
js
// bru.runner.setNextRequest controls which request runs next
if (res.body.nextPage) {
bru.runner.setNextRequest("users/get-users-page-2");
} else {
bru.runner.setNextRequest(null); // done
}
CLI
bash
npm install -g @usebruno/cli
# Run all requests in current directory
bru run
# Run single request
bru run requests/get-users.bru
# Use an environment
bru run --env production
# Recurse into subdirectories
bru run --recursive
# Filter by tag
bru run --tag smoke
# Set iteration count (data-driven)
bru run --iteration-count 5
bru run --iteration-data users.json
# Output formats
bru run --output results.json
bru run --output results.xml --output-format junit
bru run --output results.html --output-format html
# Sandbox mode
bru run --sandbox developer # enables lodash, ajv, cheerio, etc.
# Override environment variables
bru run --env-var baseUrl=https://staging.example.com
# Bail on first failure
bru run --bail
# Timeout per request (ms)
bru run --timeout 10000
# Verbose output
bru run --reporter-skip-all-headers
GitHub Actions example
yaml
- name: Run API tests
run: |
npm install -g @usebruno/cli
bru run --env ci --output results.xml --output-format junit --bail
working-directory: ./api-tests
GraphQL
meta {
name: Get User
type: graphql
seq: 1
}
post {
url: {{baseUrl}}/graphql
body: graphql
auth: bearer
}
auth:bearer {
token: {{token}}
}
body:graphql {
query {
user(id: "{{userId}}") {
id
name
email
}
}
}
body:graphql:vars {
{
"userId": "{{userId}}"
}
}
Folder-Level Config (folder.bru)
Apply shared auth, headers, scripts, and variables to all requests in a folder:
meta {
name: Users
}
auth:bearer {
token: {{token}}
}
headers {
X-API-Version: v2
}
script:pre-request {
console.log("Before request in Users folder");
}
vars {
userLimit: 20
}
Import / Export
Bruno can import from:
• Postman collections (v2.1)
• Insomnia collections
• OpenAPI / Swagger (v2, v3)
• WSDL (SOAP)
Export to OpenAPI format. Access via File → Import or the collection menu.
VS Code Extension
Available as bruno-api-client.bruno in the VS Code marketplace. Run requests directly from the IDE without opening the desktop app.