Bruno: The Open-Source Postman Alternative That Stores API Collections as Text

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.

.bru text MIT open source Git versioned CLI support

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 .bru file 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 bru CLI 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

CODA

A good tool doesn’t sell itself with a feature list — it recognizes the teams whose philosophy matches its own.

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.




Leave a Comment