Migrer Postman vers Bruno dans un mono-repo Go — le guide terrain

Le ticket arrive un lundi matin : « Migrer les collections Postman vers Bruno, les intégrer au mono-repo. » Postman, tu l'utilises depuis des années sans vraiment y penser. Bruno, tu en as entendu parler mais tu n'en as jamais ouvert. La doc officielle te liste les features. Les articles Medium t'expliquent "les 10 raisons de switcher". Aucun ne te dit concrètement comment démarrer proprement sur un mono-repo avec 6 services — patient API, labo, portail médecin, backoffice, facturation, base médicaments — 3 environnements et des secrets à ne pas commiter.

Voilà ce guide. Pas de teaser, pas de prise en main progressive — directement ce qu'il faut savoir pour que le résultat soit propre dès le premier commit.

Pourquoi Bruno

La vraie raison n'est pas "Bruno c'est mieux que Postman". C'est que Postman a dérivé vers un modèle cloud-first qui pose des problèmes concrets en équipe :

  • Les collections Postman vivent dans le cloud Postman, pas dans ton repo. Résultat : elles ne sont pas reviewables dans une PR, elles ne suivent pas les branches, et si quelqu'un modifie une requête sans prévenir, tu ne le vois nulle part.
  • Le partage de workspace nécessite un compte et une organisation Postman payante dès qu'on dépasse le plan gratuit — ce qui arrive vite à 4 devs actifs.
  • Le client Postman pèse 300+ Mo (Electron). Ça reste anecdotique, mais c'est représentatif du modèle : faire grossir l'outil pour justifier la valeur cloud.
  • Les variables d'environnement Postman sont stockées dans le cloud Postman, ce qui crée une friction pour les secrets : soit tu les mets là-bas (et tu perds le contrôle), soit tu les gères en dehors (et tu doubles la configuration).

Bruno résout ces problèmes d'une seule décision d'architecture : les collections sont des fichiers texte (format .bru, une DSL simple) stockés dans un répertoire. Pas de compte, pas de cloud, pas de sync magique. Un git clone et tout est là. Les changements de requêtes apparaissent dans les diffs, les PR, le blame. C'est la seule vraie raison de migrer.

Les concepts en 5 minutes

Bruno a trois concepts à comprendre avant d'ouvrir l'interface. Deux sont similaires à Postman, un est différent de manière importante.

Collection — un répertoire de fichiers .bru. Quand tu ouvres Bruno et que tu crées une collection, tu choisis un dossier sur ton disque. C'est tout. Chaque requête est un fichier .bru dans ce dossier ou un sous-dossier.

Environment — un ensemble de variables nommées, définies dans l'UI Bruno et stockées dans le répertoire de la collection dans un sous-dossier environments/, un fichier .bru par environnement. Ces fichiers sont versionnés. Ils contiennent les variables non-sensibles : URLs de base, ports, noms de domaine, feature flags. Tu peux y mettre base_url = http://localhost:8080, tu ne dois pas y mettre un token JWT ou un certificat client.

.env — un fichier .env classique à la racine de la collection, gitignored. Il contient les secrets : tokens, mots de passe, chemins vers des certificats. Bruno le charge automatiquement. Dans tes fichiers .bru, tu accèdes aux variables d'environnement avec {{TOKEN}} (qui cherche d'abord dans l'Environment actif, puis dans le .env) ou avec process.env.TOKEN dans les scripts.

Concept Versionné ? Usage
environments/*.bru Oui base_url, ports, noms de domaine
.env Non (gitignored) Tokens, certificats, mots de passe
.bru (requêtes) Oui Définition des appels API

Structure dans le mono-repo

Première décision : où mettre les collections dans le mono-repo. Plusieurs options : postman/ (nom legacy à éviter), api-collection/ (verbeux), api/ (ambigu si tu as déjà des packages Go qui s'appellent api), bruno/ (nom de l'outil — explicite, court, sans ambiguïté).

Le choix bruno/ a un avantage supplémentaire : il rend visible dans l'arborescence que c'est une collection Bruno, pas un répertoire de code Go. Quand quelqu'un clone le repo pour la première fois, il n'a pas à deviner ce que contient ce dossier.

Structure recommandée pour 6 services :

bruno/
├── environments/
│   ├── local.bru
│   ├── dev.bru
│   └── prod.bru
├── .env              # gitignored — tokens, certs
├── .gitignore
├── patient-api/      # Patient API — REST
│   ├── health.bru
│   ├── patients/
│   │   ├── create-patient.bru
│   │   └── get-patient.bru
│   └── ...
├── lab-service/      # Lab Service — gRPC
│   ├── lab-results.bru
│   └── ...
├── doctor-portal/    # REST
│   └── ...
├── backoffice/       # REST
│   └── ...
├── billing-service/  # REST
│   └── ...
└── drug-db-api/      # REST — API externe (ex: OpenFDA)
    └── ...

Le .gitignore dans bruno/ contient au minimum :

.env

Un service = un sous-dossier. À l'intérieur, tu peux créer autant de sous-dossiers que nécessaire pour organiser les requêtes par ressource ou par feature. Bruno affiche l'arborescence telle quelle dans son explorateur.

Gérer les environnements

Les fichiers d'environnement sont des fichiers .bru avec une syntaxe légèrement différente des requêtes. Voici à quoi ressemble un local.bru réaliste :

vars {
  patient_url: http://localhost:8080
  lab_url: localhost:9090
  doctor_url: http://localhost:8081
  backoffice_url: http://localhost:8082
  billing_url: http://localhost:8083
  drug_db_url: https://api.openfda.gov
  grpc_tls: false
}

vars:secret [
  AUTH_TOKEN
  DRUG_DB_API_KEY
]

Le bloc vars:secret liste les noms des variables secrètes — il déclare leur existence sans leur valeur. Les valeurs viennent du .env. C'est ce qui permet à Bruno de gérer l'affichage (masqué dans l'UI) sans stocker les secrets dans le fichier versionné.

Le fichier dev.bru ressemble à ceci :

vars {
  patient_url: https://patient-dev.acme-internal.com
  lab_url: lab-dev.acme-internal.com:443
  doctor_url: https://doctor-dev.acme-internal.com
  backoffice_url: https://backoffice-dev.acme-internal.com
  billing_url: https://billing-dev.acme-internal.com
  drug_db_url: https://api.openfda.gov
  grpc_tls: true
}

vars:secret [
  AUTH_TOKEN
  DRUG_DB_API_KEY
]

Pour switcher d'environnement dans Bruno : menu déroulant en haut à droite de l'interface, tu sélectionnes local, dev ou prod. Toutes les requêtes utilisent immédiatement les variables du nouvel environnement.

Les secrets dans .env

Le .env à la racine de bruno/ contient les valeurs réelles des variables secrètes déclarées dans les environnements :

AUTH_TOKEN=
DRUG_DB_API_KEY=
CLIENT_CERT_PATH=/home/user/.certs/client.crt
CLIENT_KEY_PATH=/home/user/.certs/client.key

Dans une requête .bru, tu références ces variables avec la syntaxe double-accolades :

headers {
  Authorization: Bearer {{AUTH_TOKEN}}
  X-DrugDB-Key: {{DRUG_DB_API_KEY}}
}

Bruno résout {{AUTH_TOKEN}} en cherchant dans l'ordre : variables de l'Environment actif, puis .env. Si tu as défini AUTH_TOKEN dans les deux, l'Environment gagne. C'est le comportement attendu : tu peux surcharger un secret par environnement sans modifier le .env.

Pour les certificats client (mutual TLS), Bruno supporte la configuration dans les paramètres de la collection. Tu peux référencer les chemins depuis le .env : {{CLIENT_CERT_PATH}} dans le champ de chemin de certificat.

gRPC en mode reflection

Le service de laboratoire d'Acme expose un service gRPC — les résultats d'analyses arrivent en streaming au fil du traitement, dès qu'un automate termine une mesure. La bonne nouvelle : Bruno supporte gRPC nativement, et il supporte le mode server reflection — tu n'as pas besoin du fichier .proto pour découvrir et appeler les méthodes. Le serveur répond à une requête de reflection et Bruno construit la liste des méthodes disponibles.

Pour que la reflection fonctionne, ton service Go doit avoir enregistré le service de reflection. Si ce n'est pas encore le cas, deux lignes à ajouter dans main.go :

import "google.golang.org/grpc/reflection"

// Dans la fonction qui configure le serveur gRPC :
reflection.Register(grpcServer)

Voici à quoi ressemble un fichier .bru pour une requête gRPC unaire :

meta {
  name: Get Lab Results
  type: grpc
  seq: 1
}

url: {{lab_url}}

body {
  {
    "patient_id": "{{patient_id}}",
    "test_type": "blood_panel",
    "date_from": "2026-01-01"
  }
}

grpc {
  method: LabResultService/GetResults
  reflect: true
  tls: {{grpc_tls}}
}

Le champ reflect: true dit à Bruno d'utiliser la reflection serveur pour récupérer le schéma. tls: {{grpc_tls}} utilise la variable d'environnement — false en local, true en dev/prod. L'URL est juste l'host:port sans scheme. Le patient_id utilisé dans le body peut être injecté par un script pre-request ou copié depuis une réponse REST précédente (création de dossier patient).

Pour le gRPC streaming (server-stream, client-stream, bidirectionnel), Bruno le supporte aussi mais la configuration est légèrement différente — la doc officielle couvre ce cas mieux que n'importe quel résumé.

Un vrai fichier .bru

C'est là où les gens sont le plus surpris en venant de Postman : les requêtes sont des fichiers texte avec une syntaxe simple, pas du JSON imbriqué sur 200 lignes. Voici une requête REST complète avec auth bearer, body JSON et headers custom :

meta {
  name: Create Patient Record
  type: http
  seq: 1
}

post {
  url: {{patient_url}}/v1/patients
  body: json
  auth: bearer
}

auth:bearer {
  token: {{AUTH_TOKEN}}
}

headers {
  X-Request-ID: {{$randomUUID}}
  X-Client-Version: 2.1.0
}

body:json {
  {
    "last_name": "Dupont",
    "first_name": "Marie",
    "dob": "1985-03-12",
    "nss": "{{$randomUUID}}"
  }
}

tests {
  test("status is 201", function() {
    expect(res.status).to.equal(201);
  });

  test("has patient id", function() {
    expect(res.body.patient_id).to.be.a("string");
  });
}

Quelques observations sur ce format :

  • seq contrôle l'ordre d'affichage dans l'explorateur Bruno.
  • {{$randomUUID}} est une variable dynamique intégrée — Bruno en fournit plusieurs ($timestamp, $randomInt, etc.).
  • Le bloc tests utilise Chai pour les assertions — même syntaxe que dans Postman si tu venais de l'ère pre-Postman v10.
  • Il n'y a pas de script pre-request en JSON/DSL bizarre — c'est du JavaScript dans un bloc script:pre-request { }.

Ce fichier est lisible dans un diff. Si quelqu'un change le body ou ajoute un header, tu le vois dans la PR. C'est la différence fondamentale avec Postman.

Migrer depuis Postman

Bruno supporte l'import depuis Postman Collection v2.1.

Ce qui migre bien : les requêtes REST simples, les headers, les body JSON/form-data, les variables d'environnement non-secrètes. Bruno génère un fichier .bru par requête, l'arborescence des dossiers est préservée.

Ce qui ne migre pas correctement :

  • Scripts pre-request complexes — les scripts Postman avec des appels à pm.sendRequest() ou des manipulations d'environnement avancées doivent être réécrits manuellement. La syntaxe Bruno est similaire mais pas identique.
  • Les Flows Postman — complètement Postman-spécifique, pas d'équivalent dans Bruno.
  • Les monitors et mock servers — fonctionnalités cloud Postman, hors scope de Bruno.
  • Les variables secrètes — normalement tu ne les exportes pas, donc elles n'apparaîtront pas dans le JSON exporté.

Recommandation pragmatique : importer une collection, vérifier le résultat, corriger ce qui est cassé. Ne pas faire un import en masse des 6 services sans vérifier. Les requêtes critiques (auth, création de ressources) méritent d'être recréées à la main en 5 minutes plutôt qu'importées et laissées avec des comportements imprévisibles. Le format .bru est suffisamment simple pour ça.

Le pas-à-pas

  1. Télécharger et installer Bruno — disponible sur usebruno.com/downloads pour macOS, Linux et Windows. Pas de compte à créer, pas de cloud, installation classique.
  2. Exporter depuis Postman — dans Postman, clic droit sur la collection → ExportCollection v2.1 → sauvegarder le fichier JSON quelque part en local.
  3. Créer la collection Bruno dans le repo — dans Bruno : File → Open Collection → choisir le dossier bruno/ dans le mono-repo (ou en créer un nouveau si le dossier n'existe pas encore). Bruno ouvre ce dossier comme racine de la collection.
  4. Importer la collection Postman — dans Bruno : File → Import Collection → Postman Collection v2.1 → sélectionner le JSON exporté à l'étape 2. Bruno crée un fichier .bru par requête dans le dossier courant, en préservant l'arborescence des dossiers.
  5. Créer les environnements — dans Bruno : menu Environments (icône engrenage en haut à droite) → Create Environment → nommer local. Ajouter les variables : base_url, ports, etc. Répéter pour dev et prod. Bruno crée les fichiers correspondants dans bruno/environments/.
  6. Créer le .env et le .gitignore — créer bruno/.env avec les secrets (tokens, clés API, chemins de certificats). Créer bruno/.gitignore avec au minimum .env dedans. Committer le .gitignore, ne jamais committer le .env.
  7. Vérifier et corriger — tester chaque requête importée une par une. Corriger les variables si nécessaire ({{variable}} en Postman est compatible avec Bruno, mais les scripts pre-request complexes doivent être réécrits à la main dans un bloc script:pre-request { } en JavaScript pur).
  8. Committergit add bruno/git commit -m "feat: migrate API collections from Postman to Bruno". Les fichiers .bru, environments/*.bru et .gitignore sont versionnés. Le .env ne l'est jamais.

Conclusion

La migration Postman → Bruno prend une demi-journée pour un mono-repo de cette taille. La moitié du temps c'est de la configuration initiale (structure, environnements, .env). L'autre moitié c'est de vérifier que les requêtes importées fonctionnent vraiment.

Ce que tu gagnes n'est pas "un meilleur outil API". C'est que les collections deviennent du code : reviewables dans les PRs, diffables commit par commit, cohérentes entre tous les devs sans synchronisation manuelle. Quand quelqu'un ajoute un endpoint ou change un paramètre, c'est dans le même commit que le code Go.

C'est suffisant pour justifier la migration.

Commentaires (0)