Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds
Torna alle collezioni
Guide25 min read

Bruno

Bruno is an open-source API client - Postman alternative with Git-friendly format. Complete guide.

Bruno - Open-Source API Client

Czym jest Bruno?

Bruno to nowoczesny, open-source klient API zaprojektowany jako alternatywa dla Postmana z fundamentalną różnicą w podejściu: kolekcje API są zapisywane jako pliki tekstowe w formacie .bru, które możesz commitować bezpośrednio do repozytorium Git. To sprawia, że Bruno jest idealnym narzędziem dla zespołów, które chcą wersjonować swoje kolekcje API razem z kodem źródłowym.

Bruno działa całkowicie offline, bez konieczności zakładania konta czy korzystania z chmury. Twoje dane pozostają na Twoim komputerze, co jest kluczowe dla firm dbających o bezpieczeństwo i prywatność danych.

Dlaczego Bruno?

Kluczowe zalety

  1. Git-friendly - Kolekcje jako pliki tekstowe w repozytorium
  2. Offline-first - Działa bez konta i internetu
  3. Open-source - MIT License, aktywny development
  4. Prywatność - Dane zostają lokalnie na Twoim komputerze
  5. Darmowy - Żadnych płatnych planów, wszystko za darmo
  6. Cross-platform - Windows, macOS, Linux
  7. Scripting - JavaScript do pre-request i testów
  8. Environments - Zmienne środowiskowe w plikach

Bruno vs Postman vs Insomnia

CechaBrunoPostmanInsomnia
CenaDarmowyFreemium ($14/mo)Freemium ($5/mo)
StorageLokalne plikiCloudCloud/Local
Git integrationNatywnieExport/ImportPlugin
OfflinePełnyOgraniczonyTak
Open-sourceTak (MIT)NieCzęściowo
ScriptingJavaScriptJavaScriptJavaScript
Team collaborationVia GitWbudowaneWbudowane
GraphQLTakTakTak
gRPCTakTakTak
WebSocketTakTakTak
Konto wymaganeNieTakOpcjonalne

Kiedy wybrać Bruno?

Idealne dla:

  • Zespołów używających Git
  • Firm dbających o prywatność
  • Projektów open-source
  • Środowisk bez internetu
  • Developerów ceniących prostotę

Rozważ alternatywy gdy:

  • Potrzebujesz wbudowanego team collaboration bez Git
  • Wymagasz zaawansowanych integracji enterprise
  • Preferujesz cloud-based workflow

Instalacja

Desktop App

Code
Bash
# macOS (Homebrew)
brew install bruno

# Windows (Chocolatey)
choco install bruno

# Windows (Scoop)
scoop install bruno

# Linux (Snap)
sudo snap install bruno

# Linux (AppImage)
# Pobierz z https://www.usebruno.com/downloads

CLI (Bruno CLI)

Code
Bash
# npm
npm install -g @usebruno/cli

# Weryfikacja
bru --version

Struktura projektu

Organizacja kolekcji

Bruno organizuje kolekcje w katalogach z plikami .bru:

Code
TEXT
api-collection/
├── bruno.json              # Konfiguracja kolekcji
├── environments/
│   ├── local.bru          # Środowisko lokalne
│   ├── staging.bru        # Środowisko staging
│   └── production.bru     # Środowisko produkcyjne
├── users/
│   ├── folder.bru         # Metadata folderu
│   ├── get-all-users.bru
│   ├── get-user-by-id.bru
│   ├── create-user.bru
│   ├── update-user.bru
│   └── delete-user.bru
├── posts/
│   ├── folder.bru
│   ├── list-posts.bru
│   └── create-post.bru
└── auth/
    ├── folder.bru
    ├── login.bru
    └── refresh-token.bru

Plik bruno.json

Code
JSON
{
  "version": "1",
  "name": "My API Collection",
  "type": "collection",
  "ignore": [
    "node_modules",
    ".git"
  ]
}

Format .bru

Podstawowa struktura żądania

Code
BRU
meta {
  name: Get All Users
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/users
  body: none
  auth: none
}

headers {
  Content-Type: application/json
  Accept: application/json
}

query {
  page: 1
  limit: 20
  sort: -created
}

POST z body JSON

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

post {
  url: {{baseUrl}}/api/users
  body: json
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

headers {
  Content-Type: application/json
}

body:json {
  {
    "name": "Jan Kowalski",
    "email": "jan@example.com",
    "role": "user",
    "settings": {
      "notifications": true,
      "theme": "dark"
    }
  }
}

Form Data

Code
BRU
meta {
  name: Upload Avatar
  type: http
  seq: 3
}

post {
  url: {{baseUrl}}/api/users/{{userId}}/avatar
  body: multipartForm
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

body:multipart-form {
  avatar: @file(/path/to/avatar.png)
  description: Profile photo
}

GraphQL

Code
BRU
meta {
  name: Get User with Posts
  type: graphql
  seq: 1
}

post {
  url: {{baseUrl}}/graphql
  body: graphql
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

body:graphql {
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
        createdAt
      }
    }
  }
}

body:graphql:vars {
  {
    "id": "{{userId}}"
  }
}

Environments (Środowiska)

Definicja środowiska

environments/local.bru
BRU
# environments/local.bru
vars {
  baseUrl: http://localhost:3000
  apiVersion: v1
}

vars:secret [
  accessToken,
  refreshToken,
  apiKey
]
environments/staging.bru
BRU
# environments/staging.bru
vars {
  baseUrl: https://staging-api.example.com
  apiVersion: v1
}

vars:secret [
  accessToken,
  refreshToken,
  apiKey
]
environments/production.bru
BRU
# environments/production.bru
vars {
  baseUrl: https://api.example.com
  apiVersion: v1
}

vars:secret [
  accessToken,
  refreshToken,
  apiKey
]

Używanie zmiennych

Code
BRU
get {
  url: {{baseUrl}}/api/{{apiVersion}}/users/{{userId}}
  body: none
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

Zmienne dynamiczne

Code
JavaScript
// W script:pre-request
bru.setVar("timestamp", Date.now());
bru.setVar("randomId", Math.random().toString(36).substring(7));
bru.setVar("isoDate", new Date().toISOString());

Scripting

Pre-request scripts

Code
BRU
meta {
  name: Create Order
  type: http
  seq: 1
}

post {
  url: {{baseUrl}}/api/orders
  body: json
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

body:json {
  {
    "orderId": "{{orderId}}",
    "timestamp": "{{timestamp}}",
    "items": [
      {"productId": "123", "quantity": 2}
    ]
  }
}

script:pre-request {
  // Generuj unikalne ID zamówienia
  const orderId = `ORD-${Date.now()}-${Math.random().toString(36).substring(7)}`;
  bru.setVar("orderId", orderId);

  // Timestamp
  bru.setVar("timestamp", new Date().toISOString());

  // Pobierz dane z poprzedniego response (jeśli zapisane)
  const savedToken = bru.getVar("accessToken");
  if (!savedToken) {
    console.log("Warning: No access token found");
  }
}

Post-response scripts

Code
BRU
script:post-response {
  // Zapisz token z response
  if (res.body && res.body.accessToken) {
    bru.setVar("accessToken", res.body.accessToken);
    bru.setVar("refreshToken", res.body.refreshToken);
  }

  // Zapisz ID do użycia w następnych requestach
  if (res.body && res.body.id) {
    bru.setVar("lastCreatedId", res.body.id);
  }

  // Log dla debugging
  console.log("Response status:", res.status);
  console.log("Response time:", res.responseTime, "ms");
}

Dostępne API w skryptach

Code
JavaScript
// Zmienne
bru.setVar("name", "value");         // Ustaw zmienną
const value = bru.getVar("name");    // Pobierz zmienną
bru.setEnvVar("name", "value");      // Ustaw env var
const env = bru.getEnvVar("name");   // Pobierz env var

// Request info
const url = req.getUrl();
const method = req.getMethod();
const headers = req.getHeaders();
const body = req.getBody();

// Response info (tylko w post-response)
const status = res.status;
const statusText = res.statusText;
const headers = res.headers;
const body = res.body;
const responseTime = res.responseTime;

// Utilities
const crypto = require('crypto');
const uuid = require('uuid');

// HMAC signature example
const signature = crypto
  .createHmac('sha256', bru.getVar('apiSecret'))
  .update(body)
  .digest('hex');
bru.setVar("signature", signature);

Testy

Podstawowe asercje

Code
BRU
meta {
  name: Get User
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/users/{{userId}}
  body: none
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

tests {
  // Status code
  test("should return 200 OK", function() {
    expect(res.status).to.equal(200);
  });

  // Response body
  test("should return user object", function() {
    expect(res.body).to.have.property('id');
    expect(res.body).to.have.property('email');
    expect(res.body).to.have.property('name');
  });

  // Type checking
  test("user id should be string", function() {
    expect(res.body.id).to.be.a('string');
  });

  // Value validation
  test("email should be valid format", function() {
    expect(res.body.email).to.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  });
}

Zaawansowane testy

Code
BRU
tests {
  // Array validation
  test("should return array of users", function() {
    expect(res.body.users).to.be.an('array');
    expect(res.body.users.length).to.be.greaterThan(0);
  });

  // Nested properties
  test("users should have required fields", function() {
    res.body.users.forEach(user => {
      expect(user).to.have.property('id');
      expect(user).to.have.property('email');
      expect(user).to.have.property('createdAt');
    });
  });

  // Response time
  test("response should be fast", function() {
    expect(res.responseTime).to.be.below(500);
  });

  // Headers
  test("should have correct content type", function() {
    expect(res.headers['content-type']).to.include('application/json');
  });

  // Conditional tests
  test("pagination should work", function() {
    if (res.body.meta) {
      expect(res.body.meta).to.have.property('page');
      expect(res.body.meta).to.have.property('totalPages');
      expect(res.body.meta.page).to.equal(1);
    }
  });

  // Save data for next requests
  test("save first user id", function() {
    if (res.body.users && res.body.users[0]) {
      bru.setVar("testUserId", res.body.users[0].id);
    }
  });
}

Schema validation

Code
BRU
tests {
  test("response should match schema", function() {
    const schema = {
      type: 'object',
      required: ['id', 'email', 'name', 'createdAt'],
      properties: {
        id: { type: 'string' },
        email: { type: 'string', format: 'email' },
        name: { type: 'string', minLength: 1 },
        createdAt: { type: 'string', format: 'date-time' },
        role: { type: 'string', enum: ['user', 'admin', 'moderator'] }
      }
    };

    // Bruno uses chai for assertions
    // For JSON schema validation, you might use additional libraries
    expect(res.body).to.have.all.keys('id', 'email', 'name', 'createdAt');
  });
}

Bruno CLI

Uruchamianie kolekcji

Code
Bash
# Uruchom całą kolekcję
bru run

# Uruchom konkretny folder
bru run users/

# Uruchom pojedynczy request
bru run users/get-all-users.bru

# Z konkretnym środowiskiem
bru run --env staging

# Z outputem JSON
bru run --output results.json

# Recursive (wszystkie subfolders)
bru run --recursive

Opcje CLI

Code
Bash
# Insecure mode (ignore SSL errors)
bru run --insecure

# Bail on first failure
bru run --bail

# Parallel execution
bru run --parallel

# Custom timeout (ms)
bru run --timeout 30000

# Verbose output
bru run --verbose

# Filter by tag
bru run --tag smoke-tests

CI/CD Integration

.github/workflows/api-tests.yml
YAML
# .github/workflows/api-tests.yml
name: API Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  api-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

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

      - name: Run API Tests
        run: |
          cd api-collection
          bru run --env staging --output results.json
        env:
          BRUNO_API_KEY: ${{ secrets.API_KEY }}
          BRUNO_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}

      - name: Upload Results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: api-test-results
          path: api-collection/results.json

Environment variables w CI

Code
Bash
# Eksportuj zmienne przed uruchomieniem
export BRUNO_accessToken="your-token-here"
export BRUNO_apiKey="your-api-key"

# Bruno automatycznie używa zmiennych z prefixem BRUNO_
bru run --env production

Autentykacja

Bearer Token

Code
BRU
meta {
  name: Protected Request
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/protected
  body: none
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

Basic Auth

Code
BRU
meta {
  name: Basic Auth Request
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/data
  body: none
  auth: basic
}

auth:basic {
  username: {{username}}
  password: {{password}}
}

API Key

Code
BRU
meta {
  name: API Key Request
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/data
  body: none
  auth: none
}

headers {
  X-API-Key: {{apiKey}}
}

OAuth2 Flow

Code
BRU
# 1. Login request
meta {
  name: Login
  type: http
  seq: 1
}

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

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

script:post-response {
  if (res.status === 200 && res.body.accessToken) {
    bru.setVar("accessToken", res.body.accessToken);
    bru.setVar("refreshToken", res.body.refreshToken);
    bru.setVar("tokenExpiry", res.body.expiresIn);
  }
}

tests {
  test("login successful", function() {
    expect(res.status).to.equal(200);
    expect(res.body).to.have.property('accessToken');
  });
}
Code
BRU
# 2. Refresh Token
meta {
  name: Refresh Token
  type: http
  seq: 2
}

post {
  url: {{baseUrl}}/auth/refresh
  body: json
  auth: none
}

body:json {
  {
    "refreshToken": "{{refreshToken}}"
  }
}

script:post-response {
  if (res.status === 200) {
    bru.setVar("accessToken", res.body.accessToken);
  }
}

AWS Signature v4

Code
BRU
meta {
  name: AWS S3 Request
  type: http
  seq: 1
}

get {
  url: {{awsUrl}}/bucket/object
  body: none
  auth: awsv4
}

auth:awsv4 {
  accessKeyId: {{awsAccessKeyId}}
  secretAccessKey: {{awsSecretAccessKey}}
  region: {{awsRegion}}
  service: s3
}

Zaawansowane użycie

Collection-level scripts

collection.bru
JavaScript
// collection.bru - skrypt uruchamiany dla każdego requestu
script:pre-request {
  // Dodaj timestamp do każdego requestu
  const timestamp = Date.now().toString();
  req.setHeader('X-Request-Timestamp', timestamp);

  // Log request info
  console.log(`[${req.getMethod()}] ${req.getUrl()}`);
}

script:post-response {
  // Log response info
  console.log(`Response: ${res.status} in ${res.responseTime}ms`);

  // Save to collection variables
  if (res.headers['x-request-id']) {
    bru.setVar('lastRequestId', res.headers['x-request-id']);
  }
}

Chaining requests

Code
BRU
# 1. Create user
meta {
  name: Create User
  type: http
  seq: 1
}

post {
  url: {{baseUrl}}/api/users
  body: json
  auth: bearer
}

body:json {
  {
    "name": "Test User",
    "email": "test@example.com"
  }
}

script:post-response {
  bru.setVar("createdUserId", res.body.id);
}
Code
BRU
# 2. Get created user (uses variable from previous request)
meta {
  name: Get Created User
  type: http
  seq: 2
}

get {
  url: {{baseUrl}}/api/users/{{createdUserId}}
  body: none
  auth: bearer
}

tests {
  test("user exists", function() {
    expect(res.status).to.equal(200);
    expect(res.body.id).to.equal(bru.getVar("createdUserId"));
  });
}

Tagging i organizacja

Code
BRU
meta {
  name: Critical Health Check
  type: http
  seq: 1
}

docs {
  This is a critical endpoint that should always be tested.
  Used for monitoring and alerting.
}

get {
  url: {{baseUrl}}/health
  body: none
  auth: none
}

assert {
  res.status: eq 200
  res.responseTime: lt 1000
}

Assertions (alternatywa dla tests)

Code
BRU
assert {
  res.status: eq 200
  res.body.success: eq true
  res.body.users: isArray
  res.body.users.length: gt 0
  res.responseTime: lt 500
}

Best practices

Struktura kolekcji

Code
TEXT
api-collection/
├── bruno.json
├── README.md
├── environments/
│   ├── local.bru
│   ├── development.bru
│   ├── staging.bru
│   └── production.bru
├── auth/
│   ├── folder.bru
│   ├── login.bru
│   ├── logout.bru
│   ├── refresh.bru
│   └── register.bru
├── users/
│   ├── folder.bru
│   ├── crud/
│   │   ├── create.bru
│   │   ├── read.bru
│   │   ├── update.bru
│   │   └── delete.bru
│   └── admin/
│       └── list-all.bru
└── _shared/
    ├── health-check.bru
    └── version.bru

Konwencje nazewnictwa

Code
TEXT
# Requests
[HTTP_METHOD]-[resource]-[action].bru

# Przykłady
get-users.bru
get-user-by-id.bru
post-create-user.bru
put-update-user.bru
delete-user.bru

# Foldery
lowercase-with-dashes/

Secrets management

Code
Bash
# .gitignore
environments/*.secret.bru
.env
.env.local

# Trzymaj sekrety poza repozytorium
# Używaj zmiennych środowiskowych w CI/CD

Dokumentacja w kolekcji

Code
BRU
meta {
  name: Create Order
  type: http
  seq: 1
}

docs {
  ## Create a new order

  Creates a new order in the system.

  ### Required fields:
  - `items`: Array of items with productId and quantity
  - `shippingAddress`: Delivery address object

  ### Response:
  - `201 Created`: Order created successfully
  - `400 Bad Request`: Invalid input data
  - `401 Unauthorized`: Missing or invalid token
}

post {
  url: {{baseUrl}}/api/orders
  body: json
  auth: bearer
}

Integracje

VS Code Extension

Bruno oferuje rozszerzenie VS Code do podglądu i edycji plików .bru:

Code
Bash
# Zainstaluj z VS Code Marketplace
# Szukaj: "Bruno"

Import z Postman

Code
Bash
# W Bruno App:
# File > Import Collection > Postman Collection

# Obsługiwane formaty:
# - Postman Collection v2.1
# - Postman Environment
# - OpenAPI/Swagger
# - Insomnia

Export do OpenAPI

Bruno może eksportować kolekcje do formatu OpenAPI/Swagger dla dokumentacji.

FAQ - Najczęściej zadawane pytania

Jak migrować z Postmana?

  1. W Postmanie: Export Collection (Collection v2.1)
  2. W Bruno: File > Import Collection
  3. Wybierz plik .json
  4. Sprawdź i dostosuj zmienne środowiskowe

Czy Bruno obsługuje team collaboration?

Tak, poprzez Git. Cały zespół może pracować na tej samej kolekcji:

  • Każdy ma lokalną kopię
  • Zmiany są mergowane przez Git
  • Code review dla zmian w API

Jak używać Bruno z CI/CD?

Code
Bash
# Zainstaluj CLI w pipeline
npm install -g @usebruno/cli

# Uruchom testy
bru run --env staging

# Z outputem dla raportów
bru run --output results.json

Czy mogę używać bibliotek npm w skryptach?

Bruno ma wbudowane niektóre biblioteki:

  • crypto - kryptografia
  • uuid - generowanie UUID
  • Standardowe JavaScript API

Dla bardziej zaawansowanych potrzeb rozważ pre-processing zewnętrzne.

Jak debugować skrypty?

Code
JavaScript
// Używaj console.log
script:pre-request {
  console.log("Request URL:", req.getUrl());
  console.log("Headers:", JSON.stringify(req.getHeaders(), null, 2));
  console.log("Variables:", {
    baseUrl: bru.getVar("baseUrl"),
    token: bru.getVar("accessToken")
  });
}

Podsumowanie

Bruno to doskonała alternatywa dla Postmana dla zespołów, które:

  • Używają Git - Kolekcje jako kod źródłowy
  • Cenią prywatność - Dane pozostają lokalne
  • Potrzebują offline - Pełna funkcjonalność bez internetu
  • Preferują open-source - MIT License, aktywna społeczność
  • Chcą oszczędzić - Wszystko za darmo

Bruno sprawia, że API testing staje się częścią normalnego workflow developmentu, z code review, wersjonowaniem i CI/CD integration.


Bruno - open-source API client

What is Bruno?

Bruno is a modern, open-source API client designed as a Postman alternative with a fundamental difference in approach: API collections are stored as text files in .bru format that you can commit directly to your Git repository. This makes Bruno the ideal tool for teams that want to version their API collections alongside their source code.

Bruno works entirely offline, without any need to create an account or use the cloud. Your data stays on your computer, which is crucial for companies that care about data security and privacy.

Why Bruno?

Key advantages

  1. Git-friendly - Collections as text files in your repository
  2. Offline-first - Works without an account or internet connection
  3. Open-source - MIT License, active development
  4. Privacy - Data stays locally on your computer
  5. Free - No paid plans, everything is free
  6. Cross-platform - Windows, macOS, Linux
  7. Scripting - JavaScript for pre-request scripts and tests
  8. Environments - Environment variables stored in files

Bruno vs Postman vs Insomnia

FeatureBrunoPostmanInsomnia
PriceFreeFreemium ($14/mo)Freemium ($5/mo)
StorageLocal filesCloudCloud/Local
Git integrationNativeExport/ImportPlugin
OfflineFullLimitedYes
Open-sourceYes (MIT)NoPartially
ScriptingJavaScriptJavaScriptJavaScript
Team collaborationVia GitBuilt-inBuilt-in
GraphQLYesYesYes
gRPCYesYesYes
WebSocketYesYesYes
Account requiredNoYesOptional

When to choose Bruno?

Ideal for:

  • Teams using Git
  • Companies that care about privacy
  • Open-source projects
  • Environments without internet access
  • Developers who value simplicity

Consider alternatives when:

  • You need built-in team collaboration without Git
  • You require advanced enterprise integrations
  • You prefer a cloud-based workflow

Installation

Desktop App

Code
Bash
# macOS (Homebrew)
brew install bruno

# Windows (Chocolatey)
choco install bruno

# Windows (Scoop)
scoop install bruno

# Linux (Snap)
sudo snap install bruno

# Linux (AppImage)
# Download from https://www.usebruno.com/downloads

CLI (Bruno CLI)

Code
Bash
# npm
npm install -g @usebruno/cli

# Verification
bru --version

Project structure

Collection organization

Bruno organizes collections in directories with .bru files:

Code
TEXT
api-collection/
├── bruno.json              # Collection configuration
├── environments/
│   ├── local.bru          # Local environment
│   ├── staging.bru        # Staging environment
│   └── production.bru     # Production environment
├── users/
│   ├── folder.bru         # Folder metadata
│   ├── get-all-users.bru
│   ├── get-user-by-id.bru
│   ├── create-user.bru
│   ├── update-user.bru
│   └── delete-user.bru
├── posts/
│   ├── folder.bru
│   ├── list-posts.bru
│   └── create-post.bru
└── auth/
    ├── folder.bru
    ├── login.bru
    └── refresh-token.bru

The bruno.json file

Code
JSON
{
  "version": "1",
  "name": "My API Collection",
  "type": "collection",
  "ignore": [
    "node_modules",
    ".git"
  ]
}

The .bru format

Basic request structure

Code
BRU
meta {
  name: Get All Users
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/users
  body: none
  auth: none
}

headers {
  Content-Type: application/json
  Accept: application/json
}

query {
  page: 1
  limit: 20
  sort: -created
}

POST with JSON body

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

post {
  url: {{baseUrl}}/api/users
  body: json
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

headers {
  Content-Type: application/json
}

body:json {
  {
    "name": "Jan Kowalski",
    "email": "jan@example.com",
    "role": "user",
    "settings": {
      "notifications": true,
      "theme": "dark"
    }
  }
}

Form Data

Code
BRU
meta {
  name: Upload Avatar
  type: http
  seq: 3
}

post {
  url: {{baseUrl}}/api/users/{{userId}}/avatar
  body: multipartForm
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

body:multipart-form {
  avatar: @file(/path/to/avatar.png)
  description: Profile photo
}

GraphQL

Code
BRU
meta {
  name: Get User with Posts
  type: graphql
  seq: 1
}

post {
  url: {{baseUrl}}/graphql
  body: graphql
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

body:graphql {
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
        createdAt
      }
    }
  }
}

body:graphql:vars {
  {
    "id": "{{userId}}"
  }
}

Environments

Environment definition

environments/local.bru
BRU
# environments/local.bru
vars {
  baseUrl: http://localhost:3000
  apiVersion: v1
}

vars:secret [
  accessToken,
  refreshToken,
  apiKey
]
environments/staging.bru
BRU
# environments/staging.bru
vars {
  baseUrl: https://staging-api.example.com
  apiVersion: v1
}

vars:secret [
  accessToken,
  refreshToken,
  apiKey
]
environments/production.bru
BRU
# environments/production.bru
vars {
  baseUrl: https://api.example.com
  apiVersion: v1
}

vars:secret [
  accessToken,
  refreshToken,
  apiKey
]

Using variables

Code
BRU
get {
  url: {{baseUrl}}/api/{{apiVersion}}/users/{{userId}}
  body: none
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

Dynamic variables

Code
JavaScript
// In script:pre-request
bru.setVar("timestamp", Date.now());
bru.setVar("randomId", Math.random().toString(36).substring(7));
bru.setVar("isoDate", new Date().toISOString());

Scripting

Pre-request scripts

Code
BRU
meta {
  name: Create Order
  type: http
  seq: 1
}

post {
  url: {{baseUrl}}/api/orders
  body: json
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

body:json {
  {
    "orderId": "{{orderId}}",
    "timestamp": "{{timestamp}}",
    "items": [
      {"productId": "123", "quantity": 2}
    ]
  }
}

script:pre-request {
  // Generate unique order ID
  const orderId = `ORD-${Date.now()}-${Math.random().toString(36).substring(7)}`;
  bru.setVar("orderId", orderId);

  // Timestamp
  bru.setVar("timestamp", new Date().toISOString());

  // Retrieve data from previous response (if saved)
  const savedToken = bru.getVar("accessToken");
  if (!savedToken) {
    console.log("Warning: No access token found");
  }
}

Post-response scripts

Code
BRU
script:post-response {
  // Save token from response
  if (res.body && res.body.accessToken) {
    bru.setVar("accessToken", res.body.accessToken);
    bru.setVar("refreshToken", res.body.refreshToken);
  }

  // Save ID for use in subsequent requests
  if (res.body && res.body.id) {
    bru.setVar("lastCreatedId", res.body.id);
  }

  // Log for debugging
  console.log("Response status:", res.status);
  console.log("Response time:", res.responseTime, "ms");
}

Available API in scripts

Code
JavaScript
// Variables
bru.setVar("name", "value");         // Set a variable
const value = bru.getVar("name");    // Get a variable
bru.setEnvVar("name", "value");      // Set an env var
const env = bru.getEnvVar("name");   // Get an env var

// Request info
const url = req.getUrl();
const method = req.getMethod();
const headers = req.getHeaders();
const body = req.getBody();

// Response info (only in post-response)
const status = res.status;
const statusText = res.statusText;
const headers = res.headers;
const body = res.body;
const responseTime = res.responseTime;

// Utilities
const crypto = require('crypto');
const uuid = require('uuid');

// HMAC signature example
const signature = crypto
  .createHmac('sha256', bru.getVar('apiSecret'))
  .update(body)
  .digest('hex');
bru.setVar("signature", signature);

Tests

Basic assertions

Code
BRU
meta {
  name: Get User
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/users/{{userId}}
  body: none
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

tests {
  // Status code
  test("should return 200 OK", function() {
    expect(res.status).to.equal(200);
  });

  // Response body
  test("should return user object", function() {
    expect(res.body).to.have.property('id');
    expect(res.body).to.have.property('email');
    expect(res.body).to.have.property('name');
  });

  // Type checking
  test("user id should be string", function() {
    expect(res.body.id).to.be.a('string');
  });

  // Value validation
  test("email should be valid format", function() {
    expect(res.body.email).to.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
  });
}

Advanced tests

Code
BRU
tests {
  // Array validation
  test("should return array of users", function() {
    expect(res.body.users).to.be.an('array');
    expect(res.body.users.length).to.be.greaterThan(0);
  });

  // Nested properties
  test("users should have required fields", function() {
    res.body.users.forEach(user => {
      expect(user).to.have.property('id');
      expect(user).to.have.property('email');
      expect(user).to.have.property('createdAt');
    });
  });

  // Response time
  test("response should be fast", function() {
    expect(res.responseTime).to.be.below(500);
  });

  // Headers
  test("should have correct content type", function() {
    expect(res.headers['content-type']).to.include('application/json');
  });

  // Conditional tests
  test("pagination should work", function() {
    if (res.body.meta) {
      expect(res.body.meta).to.have.property('page');
      expect(res.body.meta).to.have.property('totalPages');
      expect(res.body.meta.page).to.equal(1);
    }
  });

  // Save data for next requests
  test("save first user id", function() {
    if (res.body.users && res.body.users[0]) {
      bru.setVar("testUserId", res.body.users[0].id);
    }
  });
}

Schema validation

Code
BRU
tests {
  test("response should match schema", function() {
    const schema = {
      type: 'object',
      required: ['id', 'email', 'name', 'createdAt'],
      properties: {
        id: { type: 'string' },
        email: { type: 'string', format: 'email' },
        name: { type: 'string', minLength: 1 },
        createdAt: { type: 'string', format: 'date-time' },
        role: { type: 'string', enum: ['user', 'admin', 'moderator'] }
      }
    };

    // Bruno uses chai for assertions
    // For JSON schema validation, you might use additional libraries
    expect(res.body).to.have.all.keys('id', 'email', 'name', 'createdAt');
  });
}

Bruno CLI

Running collections

Code
Bash
# Run the entire collection
bru run

# Run a specific folder
bru run users/

# Run a single request
bru run users/get-all-users.bru

# With a specific environment
bru run --env staging

# With JSON output
bru run --output results.json

# Recursive (all subfolders)
bru run --recursive

CLI options

Code
Bash
# Insecure mode (ignore SSL errors)
bru run --insecure

# Bail on first failure
bru run --bail

# Parallel execution
bru run --parallel

# Custom timeout (ms)
bru run --timeout 30000

# Verbose output
bru run --verbose

# Filter by tag
bru run --tag smoke-tests

CI/CD integration

.github/workflows/api-tests.yml
YAML
# .github/workflows/api-tests.yml
name: API Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  api-tests:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

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

      - name: Run API Tests
        run: |
          cd api-collection
          bru run --env staging --output results.json
        env:
          BRUNO_API_KEY: ${{ secrets.API_KEY }}
          BRUNO_ACCESS_TOKEN: ${{ secrets.ACCESS_TOKEN }}

      - name: Upload Results
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: api-test-results
          path: api-collection/results.json

Environment variables in CI

Code
Bash
# Export variables before running
export BRUNO_accessToken="your-token-here"
export BRUNO_apiKey="your-api-key"

# Bruno automatically uses variables with the BRUNO_ prefix
bru run --env production

Authentication

Bearer Token

Code
BRU
meta {
  name: Protected Request
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/protected
  body: none
  auth: bearer
}

auth:bearer {
  token: {{accessToken}}
}

Basic Auth

Code
BRU
meta {
  name: Basic Auth Request
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/data
  body: none
  auth: basic
}

auth:basic {
  username: {{username}}
  password: {{password}}
}

API Key

Code
BRU
meta {
  name: API Key Request
  type: http
  seq: 1
}

get {
  url: {{baseUrl}}/api/data
  body: none
  auth: none
}

headers {
  X-API-Key: {{apiKey}}
}

OAuth2 flow

Code
BRU
# 1. Login request
meta {
  name: Login
  type: http
  seq: 1
}

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

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

script:post-response {
  if (res.status === 200 && res.body.accessToken) {
    bru.setVar("accessToken", res.body.accessToken);
    bru.setVar("refreshToken", res.body.refreshToken);
    bru.setVar("tokenExpiry", res.body.expiresIn);
  }
}

tests {
  test("login successful", function() {
    expect(res.status).to.equal(200);
    expect(res.body).to.have.property('accessToken');
  });
}
Code
BRU
# 2. Refresh Token
meta {
  name: Refresh Token
  type: http
  seq: 2
}

post {
  url: {{baseUrl}}/auth/refresh
  body: json
  auth: none
}

body:json {
  {
    "refreshToken": "{{refreshToken}}"
  }
}

script:post-response {
  if (res.status === 200) {
    bru.setVar("accessToken", res.body.accessToken);
  }
}

AWS Signature v4

Code
BRU
meta {
  name: AWS S3 Request
  type: http
  seq: 1
}

get {
  url: {{awsUrl}}/bucket/object
  body: none
  auth: awsv4
}

auth:awsv4 {
  accessKeyId: {{awsAccessKeyId}}
  secretAccessKey: {{awsSecretAccessKey}}
  region: {{awsRegion}}
  service: s3
}

Advanced usage

Collection-level scripts

collection.bru
JavaScript
// collection.bru - script executed for every request
script:pre-request {
  // Add timestamp to every request
  const timestamp = Date.now().toString();
  req.setHeader('X-Request-Timestamp', timestamp);

  // Log request info
  console.log(`[${req.getMethod()}] ${req.getUrl()}`);
}

script:post-response {
  // Log response info
  console.log(`Response: ${res.status} in ${res.responseTime}ms`);

  // Save to collection variables
  if (res.headers['x-request-id']) {
    bru.setVar('lastRequestId', res.headers['x-request-id']);
  }
}

Chaining requests

Code
BRU
# 1. Create user
meta {
  name: Create User
  type: http
  seq: 1
}

post {
  url: {{baseUrl}}/api/users
  body: json
  auth: bearer
}

body:json {
  {
    "name": "Test User",
    "email": "test@example.com"
  }
}

script:post-response {
  bru.setVar("createdUserId", res.body.id);
}
Code
BRU
# 2. Get created user (uses variable from previous request)
meta {
  name: Get Created User
  type: http
  seq: 2
}

get {
  url: {{baseUrl}}/api/users/{{createdUserId}}
  body: none
  auth: bearer
}

tests {
  test("user exists", function() {
    expect(res.status).to.equal(200);
    expect(res.body.id).to.equal(bru.getVar("createdUserId"));
  });
}

Tagging and organization

Code
BRU
meta {
  name: Critical Health Check
  type: http
  seq: 1
}

docs {
  This is a critical endpoint that should always be tested.
  Used for monitoring and alerting.
}

get {
  url: {{baseUrl}}/health
  body: none
  auth: none
}

assert {
  res.status: eq 200
  res.responseTime: lt 1000
}

Assertions (alternative to tests)

Code
BRU
assert {
  res.status: eq 200
  res.body.success: eq true
  res.body.users: isArray
  res.body.users.length: gt 0
  res.responseTime: lt 500
}

Best practices

Collection structure

Code
TEXT
api-collection/
├── bruno.json
├── README.md
├── environments/
│   ├── local.bru
│   ├── development.bru
│   ├── staging.bru
│   └── production.bru
├── auth/
│   ├── folder.bru
│   ├── login.bru
│   ├── logout.bru
│   ├── refresh.bru
│   └── register.bru
├── users/
│   ├── folder.bru
│   ├── crud/
│   │   ├── create.bru
│   │   ├── read.bru
│   │   ├── update.bru
│   │   └── delete.bru
│   └── admin/
│       └── list-all.bru
└── _shared/
    ├── health-check.bru
    └── version.bru

Naming conventions

Code
TEXT
# Requests
[HTTP_METHOD]-[resource]-[action].bru

# Examples
get-users.bru
get-user-by-id.bru
post-create-user.bru
put-update-user.bru
delete-user.bru

# Folders
lowercase-with-dashes/

Secrets management

Code
Bash
# .gitignore
environments/*.secret.bru
.env
.env.local

# Keep secrets outside the repository
# Use environment variables in CI/CD

Documentation in collection

Code
BRU
meta {
  name: Create Order
  type: http
  seq: 1
}

docs {
  ## Create a new order

  Creates a new order in the system.

  ### Required fields:
  - `items`: Array of items with productId and quantity
  - `shippingAddress`: Delivery address object

  ### Response:
  - `201 Created`: Order created successfully
  - `400 Bad Request`: Invalid input data
  - `401 Unauthorized`: Missing or invalid token
}

post {
  url: {{baseUrl}}/api/orders
  body: json
  auth: bearer
}

Integrations

VS Code extension

Bruno offers a VS Code extension for viewing and editing .bru files:

Code
Bash
# Install from VS Code Marketplace
# Search for: "Bruno"

Import from Postman

Code
Bash
# In Bruno App:
# File > Import Collection > Postman Collection

# Supported formats:
# - Postman Collection v2.1
# - Postman Environment
# - OpenAPI/Swagger
# - Insomnia

Export to OpenAPI

Bruno can export collections to OpenAPI/Swagger format for documentation purposes.

FAQ - frequently asked questions

How to migrate from Postman?

  1. In Postman: Export Collection (Collection v2.1)
  2. In Bruno: File > Import Collection
  3. Select the .json file
  4. Review and adjust environment variables

Does Bruno support team collaboration?

Yes, through Git. The entire team can work on the same collection:

  • Everyone has a local copy
  • Changes are merged through Git
  • Code review for API changes

How to use Bruno with CI/CD?

Code
Bash
# Install CLI in your pipeline
npm install -g @usebruno/cli

# Run tests
bru run --env staging

# With output for reports
bru run --output results.json

Can I use npm libraries in scripts?

Bruno has some built-in libraries:

  • crypto - cryptography
  • uuid - UUID generation
  • Standard JavaScript APIs

For more advanced needs, consider external pre-processing.

How to debug scripts?

Code
JavaScript
// Use console.log
script:pre-request {
  console.log("Request URL:", req.getUrl());
  console.log("Headers:", JSON.stringify(req.getHeaders(), null, 2));
  console.log("Variables:", {
    baseUrl: bru.getVar("baseUrl"),
    token: bru.getVar("accessToken")
  });
}

Summary

Bruno is an excellent Postman alternative for teams that:

  • Use Git - Collections as source code
  • Value privacy - Data stays local
  • Need offline access - Full functionality without internet
  • Prefer open-source - MIT License, active community
  • Want to save money - Everything is free

Bruno makes API testing a part of the normal development workflow, with code review, version control, and CI/CD integration.