How to test API's with Bruno
Installation¶
GUI¶
brew install bruno
CLI¶
- Global:
npm install -g @usebruno/cli - Local:
npm install --save-dev @usebruno/cli
Folder Structure¶
your-repo/module/src/test/
└─ bruno/
├─ bruno.json
├─ environments/
│ ├─ local.bru
│ └─ prod.bru
├─ .env # secrets for local runs (gitignored)
├─ scripts/ # shared JS helpers for tests/scripts
│ └─ utils.js
└─ api/
├─ health/
│ └─ HEAD health.bru
├─ users/
│ ├─ GET list users.bru
│ ├─ GET user by id.bru
│ ├─ POST create user.bru
│ ├─ PUT update user.bru
│ └─ DELETE user.bru
└─ workflows/
├─ LOOP get many users.bru
└─ POLL order status.bru
Note that Collections are simply folders.
Environments¶
environments/local.bru
vars {
baseUrl: http://localhost:3000
# Pull secrets from .env so they stay out of git
accessToken: \{\{process.env.JWT_TOKEN\}\}
}
Requests¶
HEAD¶
head {
url: \{\{baseUrl\}\}/health
}
tests {
test("status is 200", function () {
expect(res.status).to.equal(200);
});
test("server header present", function () {
// safer check for HEAD than relying on an empty body
const server = res.getHeader("server");
expect(server === undefined || typeof server === "string").to.be.true;
});
}
GET¶
get {
url: \{\{baseUrl\}\}/users
}
tests {
test("200 OK", function () {
expect(res.status).to.equal(200);
});
test("returns an array", function () {
expect(res.body).to.be.an("array");
});
}
POST¶
post {
url: \{\{baseUrl\}\}/users
}
headers {
content-type: application/json
}
body {
{
"name": "Alice",
"email": "alice@example.com"
}
}
tests {
test("created", function () {
expect(res.status).to.equal(201);
expect(res.body.id).to.be.a("number");
});
}
PUT¶
put {
url: \{\{baseUrl\}\}/users/\{\{userId\}\}
}
headers {
content-type: application/json
}
body {
{
"name": "Alice Smith"
}
}
tests {
test("updated", function () {
expect(res.status).to.equal(200);
expect(res.body.name).to.equal("Alice Smith");
});
}
DELETE¶
delete {
url: \{\{baseUrl\}\}/users/\{\{userId\}\}
}
tests {
test("deleted", function () {
// many APIs return 204 No Content on delete
expect([200, 202, 204]).to.include(res.status);
});
}
Authentication¶
Bearer Token¶
-
Easiest: use the Auth tab (automatic header)
At the collection or request Auth tab, select Bearer Token and set the token (ideally via a variable like
\{\{accessToken\}\}).
Bruno will sendAuthorization: Bearer <token>automatically.
- Or: just set the header in BRU
JWT with OAuth2: UI Setup¶
Since Bruno v2, OAuth2 is built-in, supports collection/folder/request scoping, auto-fetch and auto-refresh, and exposes the token as a variable.
Setup (collection-level recommended):
- In your collection’s Auth tab, choose OAuth 2.0 (v2).
- Pick your grant (e.g., Client Credentials), set token URL, client_id, client_secret (use env vars).
- Enable Auto-fetch (and Auto-refresh if your provider supports refresh tokens).
- Save with a Token ID (e.g., idp).
Bruno will inject the access token into requests automatically. You can also use it directly as \{\{$oauth2.idp.access_token\}\}
or in scripts via bru.getOauth2CredentialVar('$oauth2.idp.access_token').
Example: verify your token & call an endpoint¶
# api/users/GET me.bru
get {
url: \{\{baseUrl\}\}/me
# Auth: "Inherit" from collection (OAuth2)
}
tests {
test("authorized", function () {
expect(res.status).to.equal(200);
});
test("token is a JWT", function () {
const token = bru.getOauth2CredentialVar('$oauth2.idp.access_token');
const parts = token.split('.');
expect(parts.length).to.equal(3);
// inspect claim issuer (quick decode without external libs)
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
expect(payload.iss).to.be.a('string');
});
}
JWT with OAuth: Script Setup¶
Option1: Pure “token request + script” (most portable, works in CLI & CI)¶
- Create a request that calls the provider’s token endpoint and stores the token to a runtime var:
# users/00-get-oauth-token.bru
meta { name: OAuth token (client_credentials) type: http seq: 0 }
post { url: \{\{oauth_token_url\}\} }
headers { content-type: application/x-www-form-urlencoded }
body:form-urlencoded {
grant_type: client_credentials
client_id: \{\{client_id\}\}
client_secret: \{\{client_secret\}\}
scope: \{\{scope\}\}
}
script:post-response {
// Save for later requests in this run
bru.setVar("access_token", res.body.access_token);
}
- In your real requests, reference {{access_token}} in the header (see the 10-list-users.bru earlier).
This “two-request” approach is the recommended fallback when you want OAuth2 to work consistently in headless/CLI runs
(and it’s how the Bruno team describes current behavior differences).
Alternative: Authorization Code/Password grants)
Authorization Code / Password grants (also UI-free)
Use the same pattern make a “Get Token” request that hits your provider’s /token endpoint with grant_type=authorization_code
(with code + redirect handling in your test environment) or grant_type=password, store access_token with bru.setVar,
and use it in subsequent requests. (Bruno supports these grant types.)
Option 2: Native OAuth2 config inside .bru (no UI clicks)¶
Bruno’s files can embed auth settings; the request (or collection/folder) defines an auth block and an auth:oauth2 block.
Example (client_credentials):
# users/20-create-user.bru
meta { name: Create user type: http seq: 20 }
auth { mode: oauth2 }
auth:oauth2 {
grant_type: client_credentials
access_token_url: \{\{oauth_token_url\}\}
client_id: \{\{client_id\}\}
client_secret: \{\{client_secret\}\}
scope: \{\{scope\}\}
add_token_to: header # or "url"
header_prefix: Bearer # default is "Bearer"
token_id: credentials # optional: name the token
auto_fetch_token: true
auto_refresh_token: true
}
post { url: \{\{baseUrl\}\}/users }
body {
{ "email": "pat@example.com" }
}
With a token_id, you can also reference the token directly anywhere using:
Authorization: Bearer \{\{$oauth2.credentials.access_token\}\}
Notes:
- Field names above reflect how the community examples/maintainers show them being serialized into .bru.
Exact keys may evolve—if in doubt, create one sample through the UI in a scratch project and inspect the saved.bru
(you still don’t need to use the UI in your repo). - Some older CLI versions didn’t trigger OAuth2 auto-fetch; pattern A works everywhere.
Conditionals and Loops¶
Bruno’s scripting runtime (pre-request / post-response / tests) exposes req, res, and bru helpers including bru.runRequest
(execute another request by path name) and bru.setNextRequest (jump the collection runner to a specific next request).
You can await requests in scripts.
Loop over IDs and run a request per ID¶
get {
url: \{\{baseUrl\}\}/status # harmless placeholder call
}
script:pre-request {
// Drive: api/users/GET user by id.bru
const ids = [1, 2, 3, 4];
for (const id of ids) {
bru.setVar("userId", id);
await bru.runRequest("api/users/GET user by id"); // use path-like name
}
}
Conditional flow / polling with setNextRequest¶
get {
url: \{\{baseUrl\}\}/orders/\{\{orderId\}\}/status
}
tests {
test("follow until done", function () {
if (res.body.status === "PENDING") {
// Re-run this request next in the collection runner
bru.setNextRequest("api/workflows/POLL order status");
} else {
// Let the runner continue normally
bru.setNextRequest(null);
expect(["COMPLETED","FAILED"]).to.include(res.body.status);
}
});
}
Note
Avoid infinite loops with dynamic request hopping.
Shared Scripts¶
bruno/bruno.json
bruno/scripts/utils.js
Usage:
script:pre-request {
const { pickRandom } = require("utils.js");
const id = pickRandom([10, 11, 12]);
bru.setVar("userId", id);
}
Command Line¶
- Install Bruno CLI:
npm install -g @usebruno/cli - from the root of the collection (
bruno/):
# Run all requests in the current folder
bru run
# Or point to a subfolder or a single request
bru run api/users
bru run api/users/"GET user by id.bru"
# Pick an environment and inject additional env vars at runtime
bru run --env local --env-var userId=123
# Only run requests whose names match a pattern
bru run --grep "users"
# Fail fast on first error, add a small delay between requests
bru run --fail-fast --delay-request 200
# Generate reports (JSON/JUnit/HTML) for CI artifacts
bru run --reporter-json out/results.json \
--reporter-junit out/results.xml \
--reporter-html out/results.html
`
CLI Options¶
Core & setup¶
-h, --help show help; --version show version.
--env <name> pick an environment.
--env-var <k=v> override a variable (repeatable).
--env-file <path> use a specific .bru env file.
--sandbox <developer|safe> choose JS sandbox (default: developer).
Data/iteration: --csv-file-path <file>, --json-file-path <file>, --iteration-count <n>.
-r recursive run through subfolders.
Request selection & flow¶
--tests-only run only requests that have tests/active assertions.
--bail stop on first failure.
--delay <ms> delay between requests.
--tags <t1,t2> run requests that have all these tags.
--exclude-tags <t1,t2> skip requests that have any of these tags.
--parallel run requests in parallel.
SSL / security / networking¶
--cacert <file> add a CA certificate.
--ignore-truststore use only your custom CA(s).
--client-cert-config <file> client certificate config (mTLS).
--insecure allow insecure server connections.
--disable-cookies don’t persist/send cookies.
--noproxy disable Bruno and system proxies.
Output & reporting¶
--reporter-json <file> write JSON report.
--reporter-junit <file> write JUnit XML.
--reporter-html <file> write HTML.
--reporter-skip-all-headers omit headers in reports.
--reporter-skip-headers <h1,h2> omit specific headers.
Deprecated: -o, –output and -f, –format (use reporters instead).
Bruno Docs
OpenAPI import (separate subcommand)¶
bru import openapi --source <spec.{yaml|json}|URL> \
[--output <dir> | --output-file <file.json>] \
[--collection-name "<name>"] \
[--insecure]
Options: –source/-s, –output/-o, –output-file/-f, –collection-name/-n, and –insecure (for fetching a spec over TLS with invalid certs).
Exit codes (handy in CI)¶
| Exit Code | Meaning |
|---|---|
| 0 | success |
| 1 | a request/test/assertion failed |
| 2 | output dir does not exist |
| 3 | request chain looped endlessly |
| 4 | called outside a collection root |
| 5 | input file does not exist |
| 6 | environment does not exist |
| 7 | env override not a string/object |
| 8 | env override malformed |
| 9 | invalid output format |
| 255 | other error. |