EDIBLOG · DEVELOPER TOOL NOTE
Bruno: The Open-Source Postman Alternative That Stores API Collections as Text
Hey, I’m Eddie. Postman’s per-seat pricing was getting heavy on my budget, so I went looking for an alternative and found Bruno. The pitch is one thing — API collections are stored as text files you commit to Git. That’s the difference, and that’s the strength.
01 · THE CORE IDEAThe whole thing comes down to “text files”
Bottom line. Here’s how I summed up the Bruno-vs-Postman difference in one sentence. “It’s not about features — it’s about where the data lives.”
The moment I learned Bruno stores API collections as .bru text files, I was sold. Human-readable format, committed alongside the codebase, reviewable in PRs — that’s what mattered to me.
What I used in Postman
Cloud DB storage
What I moved to in Bruno
.bru text + Git
When I saw an actual .bru file, my first reaction was “oh, this is doable.” The file looks like this:
.bru file — a single Get User request
meta {
name: Get User Profile
type: http
seq: 1
}
get {
url: https://api.example.com/users/{{userId}}
body: none
auth: bearer
}
headers {
Accept: application/json
X-Request-ID: {{$randomUUID}}
}
auth:bearer {
token: {{authToken}}
}
vars:pre-request {
userId: 42
}
tests {
test("status is 200", function() {
expect(res.getStatus()).to.equal(200);
});
}
What I liked was that committing this file to Git is the whole workflow. Next time someone changes a URL or adds a header, I see it exactly via git diff. API changes get reviewed the same way code changes get reviewed.
For comparison, I pulled up the same request in Postman’s format. A Postman collection is one big JSON blob. Hard to read, hard to diff:
Postman JSON — same request, different format
{
"info": {
"name": "Get User Profile",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [{
"name": "Get User Profile",
"request": {
"method": "GET",
"header": [
{ "key": "Accept", "value": "application/json" },
{ "key": "X-Request-ID", "value": "{{$guid}}" }
],
"url": "https://api.example.com/users/{{userId}}",
"auth": { "type": "bearer", "bearer": [{ "key": "token", "value": "{{authToken}}" }] }
},
"event": [{
"listen": "test",
"script": { "exec": ["pm.test(\"status is 200\", () => pm.response.to.have.status(200));"] }
}]
}]
}
After looking at both formats — the human-readable text file wins on maintainability by a landslide.
02 · AT A GLANCEFour facts that matter
When I evaluate a tool I always check four things first: license, storage format, runtime environment, and paid options. Here’s how Bruno stacks up:
License
MIT
Core is fully free, commercial use unrestricted
Storage format
.bru plain text
Git-friendly / no DB dependency
Runtime
Desktop + CLI
macOS · Windows · Linux + bru CLI
Paid option
Golden Edition
One-time license / no per-seat fee
The last one — one-time license — was the biggest surprise for me. Postman bills per seat per month, which means team-level collection sharing pushes you into a paid plan. Bruno is a single purchase, done.
Install — macOS
# Desktop (Homebrew)
brew install --cask bruno
# CLI — for CI automation
npm install -g @usebruno/cli
# Or one-shot via npx
npx @usebruno/cli run ./api-tests --env staging
I also looked at the folder layout. A Bruno collection is just a folder. Even per-environment files are plain text:
My Bruno collection folder layout
api-tests/
├── bruno.json # collection metadata
├── environments/
│ ├── local.bru # local env vars
│ ├── staging.bru # staging
│ └── production.bru # production
├── auth/
│ ├── Login.bru # POST /auth/login
│ └── Refresh Token.bru
├── users/
│ ├── Get User Profile.bru
│ ├── Update User.bru
│ └── Delete User.bru
└── orders/
├── List Orders.bru
└── Create Order.bru
03 · WHEN TO PICKWorth a look if your team fits these signals
I don’t think Bruno is the right answer for every team. It only pays off when a specific set of signals overlap. Here are the five I’d watch for:
- Teams that want API collections to live inside the code repo Backend PRs ship
.brufile changes alongside code. API review folds into the code review flow. - Environments where external cloud sync is awkward due to security policy Finance, healthcare — anywhere data exfiltration is restricted. Bruno’s default is local storage.
- Teams where Postman’s per-seat cost is starting to bite Team-level sharing pushes you onto paid plans. Cost scales with headcount.
- Anyone running API regression tests in CI The
bruCLI runs the same collection. No separate environment setup. - REST-centric workflows (low GraphQL · gRPC dependency) REST and GraphQL are supported, gRPC is on the roadmap.
If three or more of those apply, I’d suggest piloting it. One pattern I particularly liked: committing environment-variable files to Git too:
environments/staging.bru — per-environment vars
vars {
baseUrl: https://staging-api.example.com
authToken: {{process.env.STAGING_AUTH_TOKEN}}
userId: 12345
}
vars:secret [
authToken
]
What I liked: variables marked as secret get committed to Git with empty values, and the real values get injected from the environment — so security stays clean.
CI integration — GitHub Actions example
name: API Regression
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm install -g @usebruno/cli
- run: bru run ./api-tests --env staging
env:
BRU_AUTH_TOKEN: ${{ secrets.STAGING_TOKEN }}
04 · KNOWN LIMITSWhere Bruno is still weak
When I recommend a tool, I make a point of naming its weaknesses too. Bruno isn’t perfect.
KNOWN LIMITATIONS
It doesn’t have Postman’s integrated ecosystem yet
SaaS-integration features are missing — real-time collaborative editing, cloud monitoring, mock servers, hosted auto-generated docs — all the signature Postman SaaS features aren’t there yet.
Git-workflow dependent — Bruno’s “real-time sync” is ultimately just git push / pull. For teams that don’t have Git workflows nailed down, this can actually add friction.
For backend or infra teams comfortable with Git, it’s a strength. For environments where designers and QA touch APIs directly, it’s a weakness. I piloted it on one backend project I run. Four out of five teammates were already Git-fluent, so it landed without friction.
Actual output from my bru CLI run
$ bru run ./api-tests --env staging
Running Folder: api-tests
✓ auth/Login.bru 127ms
✓ auth/Refresh Token.bru 89ms
✓ users/Get User Profile.bru 342ms
✓ users/Update User.bru 512ms
✓ orders/List Orders.bru 203ms
✓ orders/Create Order.bru 678ms
6 requests, 6 passed, 0 failed
Total time: 1.95s
Postman → Bruno migration
# 1. Export the collection from Postman (JSON)
# Collections → ... → Export → Collection v2.1
# 2. Open Bruno desktop
# File → Import Collection → Postman Collection
# Pick the JSON file → .bru files get generated automatically
# 3. Commit to Git
git add api-tests/
git commit -m "import: API collection from Postman"
git push
Existing Postman collections export to JSON and import directly into Bruno. My take: pilot it on one small side project first, then judge.
CODAIf I had to nail it down in one line
If I had to summarize Bruno in one sentence, this is how I’d put it. I realized this is what the tool is actually about.
Bruno’s value isn’t its features — it’s the philosophy of ‘treat API definitions like code.’ Real value only comes out for teams that buy into that philosophy.
— Eddie · Tool Observation Notes
A good tool doesn’t sell itself with a feature list — it recognizes the teams whose philosophy matches its own.
References
Bruno official
Disclaimer: A tool observation note based on public sources. Pricing and features may change over time. No ads, no affiliate links.
This post is part of an English mirror of a Korean dev experiments blog. Cross-posted for the global developer audience.