Guide

Setting up Git-based API collections.

Workflow patterns for teams that want API requests, environments, and assertions versioned alongside their code — regardless of which tool they use to author them.

May 2026

Moving from Postman to a Git-based workflow?

Postman to Bruno migration guide →

// Why Git-based API collections

Teams move API collections into Git for five reasons that surface consistently. Code review: when a request changes or an assertion is added, reviewers should see it in a pull request diff — not hear about it in Slack. Branch isolation: a feature that changes an endpoint can be developed, tested, and reviewed on a branch with matching collection changes before anything lands in main. Historical context: when something breaks, git blame and git log on the request file show exactly what changed, when, and by whom. Environment configuration: base URLs and non-secret settings live in committed files that track with the codebase. And tool portability: a Git-committed text format outlasts any single GUI tool.

This article covers workflow patterns — directory layout, environment files, PR discipline, CI integration, and secrets management — that apply across any tool that writes API requests to disk. It does not cover tool-specific migration commands or syntax. For Bruno-specific setup, including the step-by-step Postman migration, see the Postman to Bruno migration guide. For each other tool, see its own documentation.

Which tools fit this approach

Four GUI clients can participate in a Git-based workflow, to varying degrees. Bruno writes every collection as plain .bru files and a bruno.json — Git-native by design, with no account required. Insomnia supports a Git storage mode per project, writing YAML files that diff and commit cleanly alongside any repository. Hoppscotch can export collections as JSON; the export bundles requests rather than splitting them per-file, but it can be committed and diffed. Yaak stores data in a local SQLite database — it does not write plain-text collection files, which limits Git integration to manual export steps and rules out automated CI runs entirely.

For teams that want to skip GUI clients altogether: JetBrains HTTP Client (built into IntelliJ IDEA, GoLand, and WebStorm), VSCode REST Client, and httpyac use raw .http or .rest files that live directly in your repository. These are covered in the 'No GUI at all' section below.

Note:Postman's cloud-first model is not a fit for this workflow. Since Postman discontinued Scratch Pad in May 2023, all collections require an account and are stored on Postman's servers — there is no local file for Git to track. If your team is on Postman and wants to move to a Git-based workflow, see the Postman to Bruno migration guide for a step-by-step walkthrough.

// Directory structure that scales

One file per request, not one file per collection

The most consequential structural decision is file granularity. A single-file collection produces large, unreadable diffs and constant merge conflicts when multiple developers are active. One file per request means a PR that adds an assertion to GET /users/{id} touches exactly one file — a diff any reviewer can evaluate in under a minute.

Two structural principles follow from this. Organise by domain area — auth/, users/, orders/, webhooks/ — not by HTTP method. A PR scoped to the orders/ directory is immediately readable to a reviewer familiar with that domain. Second, separate environment configuration from request files. Environments belong in their own directory with one file per target.

Tool-agnostic layout (.bru / .yaml / .http)

api-tests/
├── README.md                    # how to run tests locally
├── .env.example                 # committed: placeholder keys only
├── .gitignore                   # excludes .env, results/
├── environments/
   ├── local.env
   ├── staging.env
   └── production.env
├── endpoints/
   ├── auth/
   ├── login.{bru,yaml,http}
   └── refresh-token.{bru,yaml,http}
   ├── users/
   ├── get-user.{bru,yaml,http}
   └── create-user.{bru,yaml,http}
   └── orders/
       └── list-orders.{bru,yaml,http}
└── shared/
    └── common-headers.json

Note:For the Bruno-specific layout — including bruno.json, how .bru and environments/*.json relate, and the full list of what to commit versus exclude — see the Postman to Bruno migration guide, Step 3. The same principles apply across tools, but Bruno's specifics differ from Insomnia's YAML layout.

Honest notes per tool

Bruno and Insomnia (in Git storage mode) both support per-request files natively — the layout above works out of the box. The file extension differs (.bru for Bruno, .yaml for Insomnia), but the directory principle is identical.

Hoppscotch exports an entire workspace or collection as a single JSON file. If you're committing Hoppscotch exports, you'll accept coarser diffs — a single change to one request shows as a diff inside a large JSON blob. If clean per-request diffs matter, you'll need to script a collection splitter. This is a one-time cost, but it is a cost.

Yaak stores all data in a local SQLite database. There are no individual request files to commit. Yaak can export collections to JSON for sharing, but those exports are a point-in-time snapshot, not a live-synced file on disk. Teams using Yaak for API exploration should plan a separate tool for any Git-tracked collection work.

// The environment files pattern

API tests need different base URLs and configuration for local development, staging, and production. Cloud GUI clients handle this through cloud-stored environment sets. Git-based workflow handles it differently: committed env files for non-sensitive defaults, and a gitignored .env file on each machine for actual secrets.

This separation is not Bruno-specific or Insomnia-specific — it's the same pattern used in modern web development for application configuration. The committed files travel with the repository; the .env file stays on each developer's machine, populated from the team's password manager or secrets vault.

environments/staging.env (committed — no secrets)

# Non-sensitive staging config — safe to commit
API_BASE_URL=https://api-staging.example.com
CLIENT_ID=test-client-staging
TIMEOUT_MS=10000
# Secrets come from .env (gitignored), not this file

.env.example (committed — placeholder values only)

# Copy to .env and fill in real values
# Obtain from: 1Password vault → "API Test Credentials"
API_TOKEN=your-api-token-here
OAUTH_CLIENT_SECRET=your-client-secret-here
OAUTH_CLIENT_ID=your-client-id-here

What the .gitignore must cover

Before the first git add in a new collection repository, confirm your .gitignore excludes secrets and generated output:

.gitignore (minimum for API collections)

.env
.env.local
results/
*.xml

Note:Bruno reads .env from the collection root automatically and exposes values via bru.getProcessEnv(). Insomnia reads environment values from its project Git storage. For the precise Bruno environment model — including how bru.getEnvVar() and bru.getProcessEnv() differ in scope — see the Postman to Bruno migration guide.

// Branch and PR workflow

The mental model is simple: API collection changes are code. A developer working on a new feature makes the matching collection changes on the same branch — new request files, updated assertions, modified environment variables — alongside the feature code. The PR contains both.

Why this works: Bruno's .bru files, Insomnia's YAML files, and raw .http files are all plain text. A reviewer sees a readable diff. A new assertion is a green line. A changed base URL is a clear before/after. A new environment variable appears as a simple addition — reviewable in the same thread as the code change that necessitated it.

  • Create a feature branch alongside your code changes
  • Edit the collection in your tool — Bruno desktop, Insomnia desktop, or your IDE for .http files
  • Run the relevant requests locally against a development or staging environment to verify
  • Commit the collection file changes with a descriptive message ('add POST /orders assertions for happy path')
  • Open a PR — the collection diff is part of the review, not an afterthought
  • Reviewer can check out the branch and run the collection against staging themselves
  • Merge after approval; CI should have validated the collection on the branch already

Note:GUI UX friction: most API clients write changes to disk immediately when you edit a request, but those changes still need a conscious git add and git commit before teammates can see them. Teams new to this pattern sometimes forget to commit collection changes alongside code changes. A simple team agreement — 'always commit collection changes before pushing a feature branch' — is enough. The problem is cultural, not technical.

// CI integration

Two patterns cover most teams: run-on-every-PR (the collection validates a branch before merge) and scheduled smoke tests (nightly or hourly runs against a live environment). Both require a CLI runner — which is where tool choice matters.

Bruno ships a CLI via @usebruno/cli — no separate package to install and pin. Insomnia's official CLI is inso from Kong, distributed as @kong/inso. httpyac has a standalone CLI at httpyac/httpyac that runs .http files directly. Each produces exit codes and optional JUnit XML output that CI systems can consume.

Pattern A — PR validation (GitHub Actions)

name: API Tests
on:
  pull_request:
    paths:
      - 'api-tests/**'

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run API tests (Bruno)
        run: npx @usebruno/cli@latest run api-tests --env staging --bail
        env:
          API_TOKEN: ${{ secrets.API_TOKEN_STAGING }}

      # Insomnia alternative:
      # - name: Run API tests (Insomnia)
      #   run: npx @kong/inso@latest run test --env staging --reporter junit
      #   env:
      #     API_TOKEN: ${{ secrets.API_TOKEN_STAGING }}

      # httpyac alternative:
      # - name: Run .http tests
      #   run: npx httpyac send api-tests/**/*.http --env staging --junit
      #   env:
      #     API_TOKEN: ${{ secrets.API_TOKEN_STAGING }}

Pattern B — scheduled smoke tests (GitHub Actions)

name: Nightly API Smoke
on:
  schedule:
    - cron: '0 6 * * *'   # 6am UTC daily

jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Smoke test production (Bruno)
        run: npx @usebruno/cli@latest run api-tests/smoke --env production --bail
        env:
          API_TOKEN: ${{ secrets.API_TOKEN_PROD }}

Note:Yaak has no CLI runner and cannot be wired into CI pipelines — all automation with Yaak must be done manually in the desktop app. This is a known design constraint; Yaak is a manual exploration tool. Teams using Yaak for local request authoring need a separate tool — Bruno, Insomnia, or httpyac — for the automated test layer.

Reporter output

For CI to report test results in GitHub Actions' test summary, GitLab's JUnit integration, or similar, configure JUnit XML output: Bruno uses --output results.xml --output-format junit; Insomnia's inso uses --reporter junit; httpyac uses --junit. Upload the results file as a CI artifact for debugging failed runs.

// Secrets management — doing it right

The non-negotiable rule: never commit actual secret values to the repository. Not in .env files, not in collection files, not in environment JSON, not buried inside CI YAML. A secret in a private repository is one leaked access token, one public-repo accident, or one insider threat away from being fully exposed.

Where secrets live across contexts

  • Local development — .env file at the collection root, gitignored, populated from the team's password manager (1Password, Bitwarden) or secrets vault (HashiCorp Vault, AWS Secrets Manager). Each developer maintains their own copy. The .env.example file in the repository tells them which keys to populate and where to find each value.
  • CI pipelines — repository-level or environment-level secrets in GitHub Actions Secrets, GitLab Protected Variables, or Bitbucket Deployment Variables. Injected as environment variables before the test job runs; never printed to build logs.
  • Production credentials — separate from staging, stored in a CI environment with restricted branch access. Production API tokens should not be readable from branches that only validated against staging.
  • New team members — onboarding means cloning the repository, copying .env.example to .env, and filling in values from the password manager documented in the README. The .env.example is the canonical reference; update it every time a new secret key is required.

Note:The common shortcut: putting a 'read-only test token' in a committed env file with a comment that it's safe to share. Don't. Test tokens get promoted to production, private repositories become public, and 'safe to share' credentials become incident reports. There are no shortcuts in the .env pattern — placeholder values only, always.

// What if you don't want a GUI client at all?

Some teams skip dedicated API client apps and use raw .http or .rest files directly in their IDE. Three tools support this well.

JetBrains HTTP Client is built into IntelliJ IDEA, GoLand, WebStorm, and Rider. Request files use the .http extension; gutter icons run individual requests; environment variables come from http-client.env.json files committed alongside the request files. Responses open in an editor pane. No external tool to install or maintain.

VSCode REST Client (the huachao.rest-client extension) uses .rest or .http files with a similar syntax. Send Request commands run inline from the file. Environment definitions live in VSCode workspace settings or in separate ###-delimited sections within the file itself. Widely used in Node.js and Python-heavy teams that already live in VSCode.

httpyac is the most capable of the three: a CLI tool and a VSCode extension that runs .http files both interactively and in CI. Its scripting model supports pre-request and post-response JavaScript, response chaining between named requests, and multipart uploads. The CLI produces JUnit XML output, making it a legitimate automated testing layer for teams that have moved off dedicated GUI clients.

Example .http file (works with JetBrains, VSCode REST Client, httpyac)

# @name login
POST {{baseUrl}}/auth/login
Content-Type: application/json

{
  "email": "{{userEmail}}",
  "password": "{{userPassword}}"
}

###

# @name getProfile
GET {{baseUrl}}/profile
Authorization: Bearer {{login.response.body.token}}

Note:When this fits: teams where every contributor is comfortable reading raw HTTP syntax, where IDE integration matters more than a form-based GUI, and where tooling overhead should be as close to zero as possible. When it doesn't fit: teams with non-developer contributors — QA analysts, PMs, support engineers — who need a graphical interface to construct and explore requests. The floor is real.

// What next

For comparison data on which GUI client fits your team's Git workflow needs — collaboration model, CLI runner maturity, environment variable handling, and data sovereignty — the API clients comparison covers Bruno, Insomnia, Hoppscotch, Yaak, and Postman across 14 dimensions.

If you're starting from Postman and want to move to Bruno specifically, the Postman to Bruno migration guide covers the full process — exporting collections, translating pre-request scripts, wiring bru run into CI, and establishing the Git-based team workflow conventions.

If you're still deciding whether a GUI API client is the right category at all — versus code-first libraries like REST Assured or Supertest — see the code-first vs GUI tools framing piece, which covers six decision factors including team composition, CI strategy, and contract testing needs.