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

BlockContent-Type
body:jsonapplication/json
body:texttext/plain
body:xmlapplication/xml
body:form-urlencodedapplication/x-www-form-urlencoded
body:multipart-formmultipart/form-data
body:graphqlGraphQL query
body:graphql:varsGraphQL 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)

TypeScopeStorage
RuntimeIn-memory, current runApp memory
RequestSingle request.bru file
FolderAll requests in folderfolder.bru
EnvironmentActive environmentenvironments/*.bru
CollectionEntire collectioncollection.bru
GlobalAll collectionsApp memory
Process envSystem / .env file.env file
PromptUser input at runtimeNever 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

CategoryOperators
Equalityequals, notEquals
Numericgt, gte, lt, lte, between
Stringcontains, notContains, startsWith, endsWith, matches, notMatches
ExistenceisDefined, isUndefined, isNull
TypeisNumber, isString, isBoolean, isArray, isJson
EmptinessisEmpty, isNotEmpty, isTruthy, isFalsy
Setin, notIn
Lengthlength

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}]

Cookies

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):

LibraryPurpose
axiosHTTP client for making additional requests
chaiBDD/TDD assertion library
momentDate/time parsing and formatting
uuidRFC4122 UUID generation
nanoidTiny unique string ID generator
crypto-jsCrypto standards (AES, HMAC, SHA, MD5, Base64…)
jsonwebtokenCreate and verify JWTs
tv4JSON Schema v4 validator
atob / btoaBase64 encode/decode

Developer Mode only (--sandbox=developer):

LibraryPurpose
node-fetchFetch API for Node.js
lodashUtility functions
ajvJSON Schema validator (drafts 04–2020)
cheerioHTML/XML parsing
xml2jsXML → 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.