Web Development · Developer Tooling
Bruno: The API Client That Stores Requests in Git
Bruno stores API collections as plain .bru text files in your git repo, no account or cloud sync needed. Here's how it compares to Postman and why teams switch.
Anurag Verma
7 min read
Sponsored
Every developer on your team has tested an API endpoint this week. The question is whether those requests live anywhere useful, or whether they exist in someone’s Postman workspace tied to a personal account and never committed to source control.
Bruno answers this differently. Collections are stored as plain .bru files, in a directory you commit to the repository alongside the code. When a new developer clones the repo, they have the full collection. When someone updates a request, that change shows up in the pull request diff. There’s no sync service, no account required, no API key to share.
What Bruno Is
Bruno is an open-source desktop API client built by Anoop M D. Functionally it covers the same ground as Postman or Insomnia: compose and send HTTP requests, organize them into collections, write pre-request scripts and tests, work with environments. The core difference is the storage model.
Where Postman stores collections in Postman’s cloud (or exports to a .json file you have to manually manage), Bruno writes every request to disk in a readable text format:
# collections/users/get-profile.bru
meta {
name: Get User Profile
type: http
seq: 1
}
get {
url: {{baseUrl}}/api/users/{{userId}}
body: none
auth: bearer
}
headers {
Accept: application/json
}
auth:bearer {
token: {{authToken}}
}
tests {
test("status is 200", function() {
expect(res.status).to.equal(200);
});
test("response has user data", function() {
const data = res.getBody();
expect(data.id).to.not.be.undefined;
expect(data.email).to.not.be.undefined;
});
}
The .bru format is readable in any text editor and diffs cleanly. When you change a URL or add a header, git shows exactly what changed. This is the same underlying instinct behind git workflows built for small teams: the fewer artifacts that live outside version control, the fewer things a reviewer has to take on faith.
Bruno also imports existing Postman and Insomnia collections through its UI (Import Collection), converting exported JSON into .bru files. The conversion handles requests, folders, and environment variables directly; pre-request scripts and complex auth flows sometimes need manual adjustment afterward, since Postman’s scripting API and Bruno’s bru.* API aren’t identical.
Setting Up a Collection
Install Bruno from the official site (available for macOS, Windows, and Linux). Open Bruno, create a new collection, and point it at a directory in your project. Convention varies. Most teams use bruno/ or .bruno/ at the project root.
Environment variables live in separate files that you can selectively gitignore:
# bruno/environments/local.bru
vars {
baseUrl: http://localhost:3000
authToken: dev-token-change-me
}
vars:secret [
authToken
]
The vars:secret directive marks fields as sensitive. Bruno won’t display them in the UI after entry, and the file should be in .gitignore for environments with real credentials. For shared dev environments with fake credentials, you commit the file. For production secrets, you don’t — the same split we recommend in our guide to secrets management in production applies here regardless of which API client you use.
A typical setup:
my-project/
├── src/
├── bruno/
│ ├── .gitignore # ignores production.bru
│ ├── collection.bru # collection metadata
│ ├── environments/
│ │ ├── local.bru # committed (fake creds)
│ │ ├── staging.bru # committed (staging API keys)
│ │ └── production.bru # gitignored
│ ├── auth/
│ │ ├── login.bru
│ │ └── refresh-token.bru
│ ├── users/
│ │ ├── get-profile.bru
│ │ ├── update-profile.bru
│ │ └── delete-account.bru
│ └── orders/
│ ├── create-order.bru
│ └── list-orders.bru
Running Collections in CI
Bruno ships a CLI runner (@usebruno/cli) that runs collections headlessly. This makes it straightforward to run API tests in CI:
npm install -g @usebruno/cli
# Run a specific folder
bru run bruno/users --env staging
# Run the full collection
bru run bruno --env staging --reporter json
The JSON reporter writes results to a file. Wire it into your CI pipeline to fail builds when API tests break — see our guide to optimizing GitHub Actions for speed and cost if this job ends up slowing down your workflow.
# .github/workflows/api-tests.yml
- name: Run Bruno API tests
run: |
npm install -g @usebruno/cli
bru run bruno --env staging --reporter json --output results.json
env:
BRUNO_STAGING_TOKEN: ${{ secrets.STAGING_API_TOKEN }}
Bruno reads environment variable overrides from the shell, so secrets from CI don’t have to be committed anywhere.
GraphQL and WebSocket Support
Bruno handles GraphQL queries without special configuration. If your team is still deciding between GraphQL, REST, and tRPC for a new service, our comparison of API design approaches is a useful starting point before you settle on a query shape to test against:
# collections/graphql/get-user.bru
meta {
name: Get User Query
type: graphql
seq: 1
}
post {
url: {{baseUrl}}/graphql
body: graphql
auth: bearer
}
body:graphql {
query {
user(id: "{{userId}}") {
id
name
email
orders {
id
total
}
}
}
}
body:graphql:vars {
{
"userId": "{{userId}}"
}
}
How It Compares to Postman
The meaningful differences come down to what you want from the tool:
| Bruno | Postman (Free) | Postman (Paid) | |
|---|---|---|---|
| Collection storage | Git files | Postman cloud | Postman cloud |
| Account required | No | Yes | Yes |
| CLI runner | Yes | Newman | Newman |
| Environments | Files | Cloud/exported | Cloud/exported |
| Team sharing | Via git | Via workspace | Via workspace |
| Pricing | Free/open source | Free with limits | $12+/user/month |
| GraphQL | Yes | Yes | Yes |
| Self-hosted | N/A | No | Enterprise only |
For Insomnia, the comparison is similar. Insomnia moved to requiring a cloud account for collections a couple of years back, which pushed a lot of teams toward alternatives.
The tradeoff is real: Postman has a better UI in some areas, better mock server support, and a richer ecosystem of integrations. If your team is already on Postman and getting value from it, switching costs are real and the marginal benefit of file-based storage may not justify the move.
But if you’re a team that cares about code review catching API changes, wants new developers to have working request collections the moment they clone the repo, and prefers not to depend on a SaaS product for local dev tooling, Bruno is worth evaluating.
Pre-Request Scripts and Variable Chaining
One pattern that’s genuinely useful for auth-heavy APIs is chaining: one request runs first, its response sets a variable, subsequent requests use that variable.
# auth/login.bru
meta {
name: Login
type: http
seq: 1
}
post {
url: {{baseUrl}}/api/auth/login
body: json
auth: none
}
body:json {
{
"email": "{{email}}",
"password": "{{password}}"
}
}
script:post-response {
const data = res.getBody();
bru.setVar("authToken", data.token);
bru.setVar("userId", data.user.id);
}
After the login request runs, authToken and userId are available in all subsequent requests in the same run. Run the collection in order (set seq on each request) and the auth flow works without manual copying of tokens.
The Practical Case
The value proposition is clearest for backend-heavy projects where the API surface changes frequently. When a developer changes an endpoint (adds a query parameter, renames a field, changes an auth requirement), the Bruno collection update is part of the same pull request as the code change. Reviewers see both. When someone asks “what does the staging environment authentication flow look like?”, the answer is in the repository.
That’s a simpler answer than “open Postman, switch to the shared workspace, hope whoever set it up shared it correctly.”
Frequently asked questions
- What makes Bruno different from Postman?
- Bruno stores collections as plain `.bru` files committed to your git repository, while Postman stores them in its own cloud (or requires manual JSON export). That means no account is required, requests can be reviewed in pull request diffs, and a new developer has the full collection the moment they clone the repo.
- How does Bruno handle secret credentials in environment files?
- Bruno's `vars:secret` directive marks specific fields as sensitive so they aren't displayed in the UI after entry. Teams typically commit environment files with fake local or staging credentials, but gitignore any file containing real production secrets.
- Can Bruno run API tests in a CI pipeline?
- Yes. Bruno ships a CLI runner, `@usebruno/cli`, that executes collections headlessly and can output a JSON report. This makes it straightforward to run a collection against a staging environment inside a GitHub Actions workflow and fail the build on a broken API test.
- Does Bruno support GraphQL and chained requests?
- Bruno handles GraphQL queries without extra configuration by setting a request's type to `graphql`. It also supports variable chaining through post-response scripts — for example, a login request can set an `authToken` variable with `bru.setVar()` that later requests in the same run reuse automatically.
- Is switching from Postman to Bruno worth it?
- It depends on what you value. Postman still has a richer UI, better mock servers, and a larger integration ecosystem, so switching costs may not be justified if you're already getting value from it. But teams that want API changes reviewed in pull requests and don't want local dev tooling tied to a SaaS account tend to find Bruno worth evaluating.
Sponsored
More from this category
More from Web Development
Sponsored
Discussion
Join the conversation.
Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.
Sponsored