Bruno - Un outil à la Postman - GUI et CLI
Découvrez Bruno, un outil de test d'API puissant à la Postman, avec support GUI et CLI.
Traduction automatique
Cet article fait partie de la série Construire et tester des API REST :
Journal des modifications

Cet article présente Bruno, une alternative libre et gratuite à Postman pour tester vos API. Il vous guide dans la mise en place d'une API Python FastAPI d'exemple, puis dans son test via l'interface graphique et le CLI de Bruno. Vous apprendrez à créer des requêtes, gérer des environnements et lancer des tests depuis la ligne de commande avec Docker, y compris comment gérer les problèmes réseau classiques. L'article aborde aussi l'ajout d'assertions pour valider vos tests.
Bruno est un outil comme Postman, utilisable gratuitement. Tout est stocké sur votre machine : vous pouvez donc conserver les fichiers dans votre codebase et les envoyer dans votre outil de versioning, par exemple.
Comme ces fichiers vivent dans votre repository, la partie CLI de Bruno se transforme naturellement en job de CI — voir GitLab - Exécuter Docker-out-of-Docker dans votre CI. Et pour vérifier le contrat de l'API plutôt que ses réponses, il y a Validez votre schéma OpenAPI face aux standards REST Belgif.
Lancer Bruno
Une fois installé, démarrez simplement bruno depuis la ligne de commande pour lancer l'interface :

Commençons par le commencement : créons une collection.

Et, comme on est malins, créons aussi un environnement (pour définir l'URL racine de notre site une seule fois) :

Nous voilà prêts, créons une nouvelle requête :
Récupérer une blague au hasard :
Une fois la requête créée, appuyez sur CTRL+ENTER ou cliquez sur la flèche de droite :

Pourquoi ça fonctionne
- Tout — collection, environnements, requêtes — est stocké sous forme de fichiers texte
.brudans votre repo : une requête se diffe et se relit comme du code. - La même collection s'exécute de deux façons : en mode exploration dans la GUI, ou en vérifications scriptées depuis le CLI — pas d'outil séparé à garder synchronisé.
- Une collection correspond à un dossier unique et propre (voir la section Ouvrir le projet avec VSCode ci-dessous) ; rien ne vit dans une base de données ou un compte cloud que vous ne contrôlez pas.
Installation
Pour installer la GUI de Bruno sur ma distribution Ubuntu, je lance ces commandes :
Consultez la documentation officielle Download & Install pour plus d'infos.
Optionnel - Démarrer une API de test pour essayer Bruno (cliquez pour les détails)
Bruno a besoin de quelque chose à interroger. Si vous n'avez pas d'API sous la main, voici une API de test FastAPI en une minute, empruntée à « Python - Fast API - Créez votre API JSON en Python en une minute » :
- Lancez
mkdir /tmp/fastapi && cd $_pour créer un dossier temporaire et vous y placer - Créez un
Dockerfileavec le contenu ci-dessous - Créez un
main.pyavec le contenu ci-dessous - Lancez la commande
docker build -t python-fastapi . && docker run --detach -v .:/app -p 82:82 python-fastapipour démarrer le serveur - Ouvrez un navigateur sur
http://127.0.0.1:82/jokespour voir une première blague (F5 pour en obtenir une nouvelle ; au hasard)
# We'll use the latest version of Python and the smaller image in size (i.e. `slim`)FROM python:slim
# We'll define the default folder in the image to /appWORKDIR /app
# The only dependency we need is fastapiRUN pip install --no-cache-dir 'fastapi[standard]'
# We need to copy our Python script in the imageCOPY main.py main.py
# Run Uvicorn with hot reload so any changes to our main.py script will force# the server to invalidate the cache and refresh the page.CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "82"]Ce Dockerfile crée une image Docker légère basée sur Python qui installe FastAPI, copie le script main.py dans l'image, puis démarre un serveur Uvicorn en mode rechargement automatique sur le port 82. C'est une configuration typique pour développer une API web rapidement.
from random import choicefrom fastapi import FastAPI
app = FastAPI()
jokes = [ "Why do programmers always mix up Halloween and Christmas? Because Oct 31 == Dec 25", "Why don't programmers like nature? Because it's full of bugs!", "How many programmers does it take to change a light bulb? None. That's a hardware problem.", "What do you call 8 hobbits? A hobbyte", "What is this [“hip”, ”hip”]? hip hip array!"]
@app.get("/jokes")async def get_jokes(): """ Returns a random joke from the list. """ return {"joke": choice(jokes)}
@app.get("/jokes/{joke_id}")def read_item(joke_id: int): """ Returns a specific joke (between 0 and 4). """ try: return {"joke": jokes[joke_id]} except IndexError: return {"error": f"Joke with ID {joke_id} not found."}, 404Ce code crée une API web simple avec FastAPI qui expose deux endpoints pour récupérer des blagues : l'un retourne une blague aléatoire, l'autre retourne une blague spécifique par son identifiant. Les blagues sont stockées dans une liste et l'API gère les cas où l'ID demandé n'existe pas.
Ouvrir le projet avec VSCode
En ouvrant le projet dans VSCode, on voit un nouveau dossier appelé Jokes (notre collection) avec très peu de fichiers : environments/dev.bru où se trouvent les variables de notre environnement, un fichier générique bruno.json, puis notre requête dans Get a random joke.bru.
Structure très propre, non ?

Exécuter des requêtes depuis la ligne de commande
Bruno propose une image Docker : elle va nous aider à automatiser l'exécution de nos requêtes depuis la ligne de commande.
Attention
À la mi-juillet 2025, je n'ai pas réussi à faire fonctionner cette image comme prévu. Je tombais sur des erreurs Cannot read properties of undefined (reading 'headers') alors que, à mon avis, tout était correctement configuré.
J'ai donc cherché une autre image et j'ai trouvé celle-ci : davidkarlsen/bruno-image. Pas de chance non plus, même la dernière version de l'époque (2.7.0) posait problème.
En regardant le Dockerfile, j'ai vu que le fichier était vraiment simple et qu'une version plus récente de Bruno était sortie : la 2.8.
Bref, je vais créer ma propre image Docker et voir si ça va mieux.
Créer notre propre image Bruno CLI
Créons un fichier bruno.Dockerfile avec le contenu suivant :
FROM node:lts-bookworm-slim// highlight-next-lineRUN npm install -g @usebruno/cli@2.8.0ENTRYPOINT ["bru"]CMD ["run"]Ce Dockerfile crée une image Docker basée sur Node.js LTS avec l'environnement Debian Bookworm allégé. Il installe l'outil CLI Bruno (un client API) globalement, puis configure le conteneur pour exécuter la commande bru par défaut.
On crée notre image comme ceci : docker build --file bruno.Dockerfile -t bruno-image . (on peut la vérifier en lançant docker run -it --rm bruno-image --version ; on devrait voir 2.8.0).
Comme on vient de le voir, notre collection est stockée dans le dossier Jokes et notre environnement dans environments/dev.bru. Mais un container ne peut pas joindre 127.0.0.1 sur votre host : créez donc un second fichier d'environnement, environments/dev-docker.bru, qui pointe vers host.docker.internal :
vars {
host: http://host.docker.internal:82
}
Lancez maintenant la collection avec cet environnement :

Sous le capot (à sauter si vous voulez juste l'utiliser)
Pourquoi ne pas simplement réutiliser environments/dev.bru, celui que la GUI utilise déjà ? Parce que l'exécuter tel quel depuis le CLI échoue :

Regardez le message d'erreur connect ECONNREFUSED 127.0.0.1:82 : le container Bruno CLI essaie de joindre le serveur web sur 127.0.0.1, mais cette adresse, c'est le container lui-même, pas votre host. environments/dev.bru ressemble à ceci :
vars {
root: http://127.0.0.1:82
}
Ça fonctionne depuis la GUI, parce que la GUI tourne directement sur votre host — là, 127.0.0.1 est votre machine. Un curl -v http://127.0.0.1:82/jokes depuis la ligne de commande de votre host fonctionne aussi :
Un container a son propre namespace réseau : 127.0.0.1 à l'intérieur n'atteint jamais l'host. C'est exactement ce que corrigent environments/dev-docker.bru et host.docker.internal ci-dessus. Effet de bord : ce nouveau fichier d'environnement ne fonctionnera pas depuis la GUI de Bruno — on peut sélectionner dev et tout marche, mais pas dev-docker :
Ajouter quelques assertions
L'intérêt de l'outil CLI, bien sûr, c'est de pouvoir lancer des assertions depuis la ligne de commande et s'assurer que l'API fonctionne toujours.
Mettons à jour le fichier Get a random joke.bru comme ceci :
meta { name: Get a random joke type: http seq: 1}
get { url: {{host}}/jokes body: none auth: none}
<!-- highlight-start -->assert { res.status: eq 200 res.headers["content-type"]: isDefined res.headers["content-type"]: isString res.headers["content-type"]: contains "application/json" res.body.joke.length: gt 0 res.body.joke: isString}<!-- highlight-end -->
settings { encodeUrl: true}Ce fichier définit une requête HTTP GET vers un endpoint de blagues et valide que la réponse est correcte — le statut doit être 200, l'en-tête content-type doit être défini et contenir 'application/json', et le corps doit contenir une propriété joke qui est une chaîne non vide.

Conclusion
Bruno vous offre un workflow à la Postman — collections, environnements, assertions — sans quitter votre repository : chaque requête est un fichier .bru que vous pouvez differ, relire et versionner comme le reste de votre code. La GUI sert à explorer et à construire une collection ; le CLI, emballé dans votre propre image Docker, transforme cette même collection en vérification reproductible.
Cette reproductibilité, c'est exactement ce qu'attend un pipeline de CI. Voir GitLab - Exécuter Docker-out-of-Docker dans votre CI pour lancer cette image CLI comme job, et Validez votre schéma OpenAPI face aux standards REST Belgif pour vérifier le contrat de l'API, pas seulement ses réponses.
Cet article fait partie de la série Construire et tester des API REST :
Chargement des commentaires…



