ITSkillsCenter
Développement Web

Configurer un workspace Bun pas-à-pas : tutoriel 2026

11 min de lecture

📍 Article principal : Bun monorepo et publication de packages : guide complet 2026

Introduction

Un développeur freelance basé à Saint-Louis veut structurer son code commun (helpers de validation UEMOA, formatage FCFA, client SMS Hubtel) pour le réutiliser entre projets clients. Plutôt que de copier-coller à chaque mission, il décide d’extraire ces utilitaires dans un workspace Bun. En une après-midi, il a une structure propre avec deux apps de démonstration et un package partagé fonctionnel. Ce tutoriel reproduit cette démarche pas-à-pas : installation des outils, initialisation du dépôt, création des workspaces, première dépendance interne, scripts d’orchestration, et vérification du tout. À la fin, vous avez un monorepo Bun fonctionnel sur lequel construire vos vrais projets.

Prérequis

  • Bun 1.2+ installé (curl -fsSL https://bun.sh/install | bash)
  • Git configuré avec votre identité
  • Éditeur compatible TypeScript (VS Code, Neovim avec LSP, ou WebStorm)
  • Connaissance de base TypeScript et npm
  • Niveau : débutant à intermédiaire — Temps : 45 minutes

Étape 1 — Initialiser le dépôt

On crée le dossier racine et on initialise le projet avec Bun. Cette première étape ressemble à un init npm classique mais Bun ajoute son lockfile binaire et sa configuration spécifique. Le nom du package racine sert d’identifiant interne — il ne sera jamais publié, donc on choisit un nom court qui décrit le projet global.

mkdir mon-monorepo && cd mon-monorepo
git init
bun init -y

Bun crée un package.json, un tsconfig.json, et un .gitignore. On modifie le package.json pour activer les workspaces et marquer le projet comme privé (jamais publiable depuis la racine). On déclare aussi les dossiers apps et packages qui contiendront les sous-projets. Le champ "private": true est non négociable — sans lui, une publication accidentelle exposerait toute la racine du dépôt à npm public.

Pour la version de Bun supportée, on ajoute "engines": { "bun": ">=1.2.0" }. Cette déclaration est lue par les CI et par certains outils, et elle documente la version minimale attendue. Pour une équipe distribuée, c’est une bonne pratique qui évite les divergences entre machines de développement.

Étape 2 — Créer la structure des dossiers

On crée les dossiers apps et packages à la racine. Cette séparation conceptuelle distingue les applications déployables des bibliothèques réutilisables. Pour ce tutoriel, on initialise une app web qui sera un serveur Hono minimal, et un package shared qui contiendra des utilitaires.

mkdir -p apps/web packages/shared
cd packages/shared
bun init -y
cd ../../apps/web
bun init -y
cd ../..

Chaque sous-projet a son propre package.json. Pour le package partagé, on adopte la convention de nom scopé : "name": "@itskills/shared". Pour l’app, on garde un nom simple : "name": "web". Le scope @itskills est arbitraire — choisissez celui qui correspond à votre organisation, votre nom de freelance, ou simplement votre identifiant GitHub.

L’arborescence finale après cette étape ressemble à un monorepo classique avec des fichiers de configuration sains. La structure reste minimale et lisible — pas de dossiers supplémentaires créés « au cas où ». On ajoute progressivement ce dont on a besoin, jamais en anticipation.

Étape 3 — Premier package partagé

On crée maintenant un utilitaire concret dans le package shared : un validateur de numéro de téléphone UEMOA qui accepte les formats Sénégal, Côte d’Ivoire, Mali et Burkina Faso. Cet utilitaire servira de cas d’usage pour démontrer la consommation entre workspaces.

// packages/shared/src/telephone.ts
export type PaysUEMOA = 'SN' | 'CI' | 'ML' | 'BF';

const prefixes: Record<PaysUEMOA, string> = {
  SN: '+221', CI: '+225', ML: '+223', BF: '+226'
};

export function validerTelephone(numero: string, pays: PaysUEMOA): boolean {
  const nettoye = numero.replace(/[\s-]/g, '');
  const prefixe = prefixes[pays];
  if (!nettoye.startsWith(prefixe)) return false;
  const local = nettoye.slice(prefixe.length);
  return /^[0-9]{8,10}$/.test(local);
}

export function formaterTelephone(numero: string, pays: PaysUEMOA): string {
  const nettoye = numero.replace(/[\s-]/g, '');
  if (validerTelephone(nettoye, pays)) {
    const prefixe = prefixes[pays];
    const local = nettoye.slice(prefixe.length);
    return prefixe + ' ' + local.match(/.{1,2}/g)?.join(' ');
  }
  return numero;
}

On expose ces fonctions via un index.ts qui réexporte tout ce qui est public. Cette indirection permet de réorganiser les fichiers internes du package sans casser les consommateurs.

// packages/shared/src/index.ts
export { validerTelephone, formaterTelephone } from './telephone';
export type { PaysUEMOA } from './telephone';

Dans le package.json du package, on configure le point d’entrée pour pointer vers le source TypeScript directement. Cette approche fonctionne parfaitement entre workspaces Bun et évite l’étape de build pendant le développement. Pour la publication ultérieure, on ajoutera un script de build, mais pas maintenant.

Étape 4 — Consommer le package depuis l’app

On retourne dans apps/web et on ajoute la dépendance interne. La syntaxe "workspace:*" indique à Bun de résoudre vers la version locale du workspace, sans aller chercher sur npm.

cd apps/web
bun add hono "@itskills/shared@workspace:*"

Bun met à jour le package.json de l’app et crée le lien interne. On peut maintenant importer les fonctions du package partagé comme depuis n’importe quelle dépendance npm classique. La résolution est instantanée et toute modification dans le package partagé est visible immédiatement, sans rebuild.

// apps/web/src/index.ts
import { Hono } from 'hono';
import { validerTelephone, formaterTelephone } from '@itskills/shared';

const app = new Hono();

app.get('/api/verif/:tel', (c) => {
  const tel = c.req.param('tel');
  const valide = validerTelephone(tel, 'SN');
  return c.json({ tel, valide, format: valide ? formaterTelephone(tel, 'SN') : null });
});

export default { fetch: app.fetch, port: 3000 };

On lance l’app : bun run --cwd apps/web src/index.ts. Le serveur démarre en moins de 200 ms et l’endpoint répond instantanément. On peut tester avec curl http://localhost:3000/api/verif/+221771234567 qui doit retourner un JSON avec valide: true.

Étape 5 — Scripts orchestrateurs

Pour ne pas avoir à se rappeler des commandes complexes à chaque session de dev, on définit des scripts à la racine qui orchestrent les sous-projets. Le package.json racine accueille des scripts qui utilisent le filtrage Bun pour cibler les bons workspaces.

{
  "scripts": {
    "dev": "bun run --filter './apps/*' --parallel dev",
    "build": "bun run --filter './apps/*' build",
    "test": "bun test",
    "typecheck": "bun --filter '*' run typecheck"
  }
}

Chaque app et package définit ses propres scripts dev, build, typecheck. Le filtrage à la racine les exécute en parallèle ou en série selon les besoins. Pour un développement quotidien, bun run dev à la racine démarre tout ce dont on a besoin sans détails à mémoriser.

Pour le typage strict, on configure le tsconfig.json racine en mode strict avec les paths résolus vers les workspaces. Chaque sous-projet étend cette base via "extends": "../../tsconfig.json". Cette approche évite la duplication de configuration et garantit que toutes les apps respectent les mêmes règles TypeScript.

Étape 6 — Configurer Git proprement

Le .gitignore racine doit ignorer les node_modules de tous les workspaces, le bun.lockb n’est jamais ignoré (au contraire, on le commit pour reproductibilité), et les fichiers d’environnement .env sont systématiquement exclus. Une configuration minimale efficace tient en quelques lignes mais demande la rigueur de la mettre en place dès le début.

# .gitignore
node_modules/
**/node_modules/
*.log
.env
.env.local
.DS_Store
.bun/
dist/
**/dist/
.svelte-kit/
**/.svelte-kit/

Pour les commits, adopter une convention claire dès le début paye sur la durée. Conventional Commits (feat:, fix:, chore:, docs:) facilite la génération automatique de changelog si on publie des packages, et structure naturellement les revues de code. Pour les équipes distribuées, c’est aussi un protocole partagé qui évite les ambiguïtés de communication.

Étape 7 — Vérification finale

Trois vérifications avant de considérer le setup terminé. Premièrement, bun install à la racine doit installer toutes les dépendances de tous les workspaces sans erreur, en moins de dix secondes pour un projet à ce stade. Deuxièmement, le serveur web doit démarrer et répondre correctement aux appels qui utilisent les fonctions du package partagé. Troisièmement, modifier une fonction dans packages/shared/src/telephone.ts et observer que le serveur web reflète le changement immédiatement (avec hot reload ou redémarrage manuel selon la configuration).

Si une de ces trois vérifications échoue, le problème vient généralement d’une de ces sources : workspaces mal déclarés dans le package.json racine, syntaxe workspace:* manquante dans la dépendance interne, ou typo dans le nom du package scopé. Le journal d’erreur de Bun est généralement explicite et pointe la cause directe.

Erreurs fréquentes

Erreur Cause Solution
« Cannot find module @itskills/shared » Workspace mal déclaré ou install non lancé bun install à la racine après ajout du workspace
Types non résolus dans VS Code Cache TypeScript périmé Recharger la fenêtre VS Code (Cmd+Shift+P, Reload)
Bun ne trouve pas le binaire PATH non mis à jour après install Relancer le terminal ou ajouter ~/.bun/bin au PATH
Conflit de version sur dépendance commune Versions divergentes entre workspaces Aligner les versions via bun update à la racine
Tests d’un package ne tournent pas bun test ne descend pas dans les workspaces par défaut Configurer testMatch dans bunfig.toml

Adaptation au contexte ouest-africain

Trois aspects pratiques pour les freelances et petites équipes ouest-africaines. Premièrement, démarrer un monorepo Bun consomme moins de 100 Mo de bande passante pour le premier bun install sur le projet de ce tutoriel — comparable à Node + pnpm équivalent mais avec une vitesse supérieure, ce qui aide quand on travaille depuis un cybercafé ou avec un forfait limité. Deuxièmement, la structure permet d’extraire facilement le package @itskills/shared vers un registre privé Verdaccio quand le freelance commence à servir plusieurs clients qui peuvent bénéficier des mêmes utilitaires. Troisièmement, l’absence de configuration build complexe rend l’onboarding d’un nouveau collaborateur trivial : cloner le dépôt, lancer bun install et bun run dev, et c’est parti. Pour des équipes distribuées entre Saint-Louis, Dakar et Touba, cette simplicité réduit les frictions de collaboration.

Tutoriels frères

Pour aller plus loin

FAQ

Faut-il commiter le bun.lockb ?
Oui, toujours. Le lockfile garantit la reproductibilité des installations entre développeurs et environnements. Sans lui, deux installs successifs peuvent produire des arborescences de dépendances différentes.

Comment renommer un workspace après création ?
Modifier le name dans le package.json du workspace, puis mettre à jour toutes les dépendances internes qui référencent l’ancien nom, et relancer bun install à la racine.

Peut-on mélanger Bun et npm dans un monorepo ?
Techniquement oui, mais c’est déconseillé. Le mélange des lockfiles (bun.lockb et package-lock.json) provoque des incohérences difficiles à diagnostiquer. Choisir un gestionnaire et s’y tenir.

Étendre le workspace pour un vrai projet

Une fois le squelette en place, le passage à un projet réel demande quelques ajouts naturels. D’abord, un second workspace d’app pour démontrer la consommation parallèle du package partagé : par exemple une app worker qui traite des paiements en arrière-plan, partageant la validation des numéros avec l’API HTTP. Ensuite, un second package partagé spécialisé : un client SMS unifié qui encapsule Hubtel, Africa’s Talking et Twilio derrière une interface unique, exploité aussi bien par l’API que par le worker. Enfin, un dossier scripts à la racine qui contient les outils de maintenance — réinitialisation des caches, génération de données de test, audit des dépendances obsolètes. Cette structure ouverte supporte ensuite la croissance organique du projet sans réorganisation lourde.

Pour la documentation interne, créer un fichier README.md dans chaque workspace décrivant son rôle, ses dépendances, et les commandes principales évite les questions répétées en équipe. Pour les agences ouest-africaines qui forment régulièrement de nouveaux développeurs juniors, ces README sont autant de points d’entrée pédagogiques qui accélèrent la montée en compétence. Une bonne pratique consiste à imposer un README à toute création de nouveau workspace via une checklist de revue de PR.

Conventions de code et linting

Centraliser les conventions de code à la racine du monorepo garantit que toutes les apps et packages respectent les mêmes règles. La configuration ESLint et Prettier vit dans des fichiers à la racine, et chaque workspace l’hérite automatiquement via la résolution de configuration ESLint qui remonte les dossiers. On choisit des règles strictes mais pragmatiques : pas de variables non utilisées, pas de imports cycliques, pas de console.log en production, types stricts. Pour Prettier, on adopte les valeurs par défaut sauf préférence d’équipe documentée.

Pour automatiser la vérification, on installe husky et lint-staged à la racine. À chaque commit, lint-staged lance ESLint et Prettier sur les fichiers modifiés uniquement, ce qui reste rapide même sur un gros monorepo. Cette discipline élimine 95 % des reviews de PR consacrées au formatage et permet aux développeurs de se concentrer sur le fond.

Besoin d'un site web ?

Confiez-nous la Création de Votre Site Web

Site vitrine, e-commerce ou application web — nous transformons votre vision en réalité digitale. Accompagnement personnalisé de A à Z.

À partir de 250.000 FCFA
Parlons de Votre Projet
Publicité