Skip to main content
Technology & EngineeringApi Testing231 lines

Bruno

Bruno API client for git-friendly, offline-first API testing with Bru markup language

Quick Summary36 lines
You are an expert in Bruno for building and testing APIs with a git-native, offline-first workflow.

## Key Points

- name: Install Bruno CLI
- name: Run API tests
- Commit the entire Bruno collection directory to git alongside your API source code.
- Use `vars:secret` in environment files for tokens and passwords — they are not saved to disk.
- Use `seq` in meta blocks to control execution order when running a folder.
- Use `script:post-response` to chain requests by storing values in collection or environment variables.
- Keep one `.bru` file per request and organize by resource folder to match your API structure.
- Review `.bru` file diffs in pull requests — they are human-readable plain text.
- Forgetting to set `seq` ordering, causing requests to run in arbitrary order when folder-level execution depends on chaining.
- Using `bru.setVar` (collection variable) when `bru.setEnvVar` (environment variable) is needed, or vice versa — they have different scopes and lifetimes.
- Not selecting the correct environment in the GUI or CLI before running, leading to empty `{{variables}}`.
- Editing `.bru` files by hand with incorrect syntax — the Bru format is whitespace-sensitive in body blocks.

## Quick Example

```bash
# macOS
brew install bruno

# npm (CLI runner)
npm install -g @usebruno/cli
```

```json
{
  "version": "1",
  "name": "My API",
  "type": "collection"
}
```
skilldb get api-testing-skills/BrunoFull skill: 231 lines
Paste into your CLAUDE.md or agent config

Bruno — API Testing

You are an expert in Bruno for building and testing APIs with a git-native, offline-first workflow.

Core Philosophy

Overview

Bruno is an open-source API client that stores collections as plain files on your filesystem using its own Bru markup language. Unlike cloud-synced tools, Bruno collections live in your repository, are version-controlled with git, and work fully offline. It supports scripting, environment variables, assertions, and a CLI runner.

Setup & Configuration

Installation

# macOS
brew install bruno

# npm (CLI runner)
npm install -g @usebruno/cli

The desktop app is available from usebruno.com for Windows, macOS, and Linux.

Collection structure

A Bruno collection is a directory of .bru files:

api-collection/
  bruno.json                  # Collection metadata
  environments/
    dev.bru
    staging.bru
  auth/
    login.bru
    refresh-token.bru
  users/
    list-users.bru
    create-user.bru
    get-user.bru

bruno.json

{
  "version": "1",
  "name": "My API",
  "type": "collection"
}

Environment files

# environments/dev.bru
vars {
  base_url: http://localhost:3000/api
  auth_email: test@test.com
  auth_password: secret123
}

vars:secret [
  auth_token
]

Core Patterns

Basic request (.bru file)

meta {
  name: List Users
  type: http
  seq: 1
}

get {
  url: {{base_url}}/users
  body: none
  auth: bearer {{auth_token}}
}

headers {
  Accept: application/json
}

query {
  page: 1
  per_page: 25
}

assert {
  res.status: eq 200
  res.body.length: gt 0
}

tests {
  test("response contains users array", function() {
    const data = res.getBody();
    expect(data).to.be.an('array');
    expect(data[0]).to.have.property('id');
  });
}

POST request with JSON body

meta {
  name: Create User
  type: http
  seq: 2
}

post {
  url: {{base_url}}/users
  body: json
  auth: bearer {{auth_token}}
}

body:json {
  {
    "name": "Alice",
    "email": "alice@example.com",
    "role": "member"
  }
}

assert {
  res.status: eq 201
  res.body.name: eq Alice
  res.body.id: isDefined
}

script:post-response {
  bru.setVar("created_user_id", res.body.id);
}

Pre-request scripting for auth

meta {
  name: Login
  type: http
  seq: 1
}

post {
  url: {{base_url}}/auth/login
  body: json
  auth: none
}

body:json {
  {
    "email": "{{auth_email}}",
    "password": "{{auth_password}}"
  }
}

script:post-response {
  const body = res.getBody();
  bru.setEnvVar("auth_token", body.token);
}

Running with the CLI

# Run entire collection
bru run --env dev

# Run a specific folder
bru run auth/ --env dev

# Run a single request
bru run users/create-user.bru --env dev

# Output as JUnit for CI
bru run --env dev --reporter junit --output results.xml

CI/CD integration (GitHub Actions)

- name: Install Bruno CLI
  run: npm install -g @usebruno/cli

- name: Run API tests
  run: bru run api-collection/ --env ci --reporter junit --output results.xml
  env:
    BASE_URL: ${{ secrets.API_BASE_URL }}

Best Practices

  • Commit the entire Bruno collection directory to git alongside your API source code.
  • Use vars:secret in environment files for tokens and passwords — they are not saved to disk.
  • Use seq in meta blocks to control execution order when running a folder.
  • Use script:post-response to chain requests by storing values in collection or environment variables.
  • Keep one .bru file per request and organize by resource folder to match your API structure.
  • Review .bru file diffs in pull requests — they are human-readable plain text.

Common Pitfalls

  • Forgetting to set seq ordering, causing requests to run in arbitrary order when folder-level execution depends on chaining.
  • Using bru.setVar (collection variable) when bru.setEnvVar (environment variable) is needed, or vice versa — they have different scopes and lifetimes.
  • Not selecting the correct environment in the GUI or CLI before running, leading to empty {{variables}}.
  • Editing .bru files by hand with incorrect syntax — the Bru format is whitespace-sensitive in body blocks.
  • Storing secrets directly in environment .bru files that get committed to version control.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add api-testing-skills

Get CLI access →