Pour commencer, importez des fichiers HTML et passez-les à l'option routes dans Bun.serve().
import { serve } from "bun";
import dashboard from "./dashboard.html";
import homepage from "./index.html";
const server = serve({
routes: {
// ** Importations HTML **
// Bundle et route index.html vers "/". Cela utilise HTMLRewriter pour scanner
// le HTML pour les balises `<script>` et `<link>`, exécute le bundler
// JavaScript & CSS de Bun, transpile tout TypeScript, JSX et TSX,
// abaisse le CSS avec l'analyseur CSS de Bun et sert le résultat.
"/": homepage,
// Bundle et route dashboard.html vers "/dashboard"
"/dashboard": dashboard,
// ** Points de terminaison API ** (Bun v1.2.3+ requis)
"/api/users": {
async GET(req) {
const users = await sql`SELECT * FROM users`;
return Response.json(users);
},
async POST(req) {
const { name, email } = await req.json();
const [user] = await sql`INSERT INTO users (name, email) VALUES (${name}, ${email})`;
return Response.json(user);
},
},
"/api/users/:id": async req => {
const { id } = req.params;
const [user] = await sql`SELECT * FROM users WHERE id = ${id}`;
return Response.json(user);
},
},
// Activer le mode développement pour :
// - Messages d'erreur détaillés
// - Rechargement à chaud (Bun v1.2.3+ requis)
development: true,
});
console.log(`Écoute sur ${server.url}`);bun run app.tsRoutes HTML
Importations HTML comme routes
Le web commence avec HTML, et le serveur de développement fullstack de Bun aussi.
Pour spécifier les points d'entrée de votre frontend, importez des fichiers HTML dans vos fichiers JavaScript/TypeScript/TSX/JSX.
import dashboard from "./dashboard.html";
import homepage from "./index.html";Ces fichiers HTML sont utilisés comme routes dans le serveur de développement de Bun que vous pouvez passer à Bun.serve().
Bun.serve({
routes: {
"/": homepage,
"/dashboard": dashboard,
},
fetch(req) {
// ... requêtes api
},
});Lorsque vous faites une requête à /dashboard ou /, Bun bundle automatiquement les balises <script> et <link> dans les fichiers HTML, les expose comme routes statiques et sert le résultat.
Exemple de traitement HTML
Un fichier index.html comme ceci :
<!DOCTYPE html>
<html>
<head>
<title>Accueil</title>
<link rel="stylesheet" href="./reset.css" />
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./sentry-and-preloads.ts"></script>
<script type="module" src="./my-app.tsx"></script>
</body>
</html>Devient quelque chose comme ceci :
<!DOCTYPE html>
<html>
<head>
<title>Accueil</title>
<link rel="stylesheet" href="/index-[hash].css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/index-[hash].js"></script>
</body>
</html>Intégration React
Pour utiliser React dans votre code côté client, importez react-dom/client et effectuez le rendu de votre application.
import dashboard from "../public/dashboard.html";
import { serve } from "bun";
serve({
routes: {
"/": dashboard,
},
async fetch(req) {
// ...requêtes api
return new Response("bonjour le monde");
},
});import { createRoot } from 'react-dom/client';
import App from './app';
const container = document.getElementById('root');
const root = createRoot(container!);
root.render(<App />);<!DOCTYPE html>
<html>
<head>
<title>Tableau de bord</title>
<link rel="stylesheet" href="../src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="../src/frontend.tsx"></script>
</body>
</html>import { useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
return (
<div>
<h1>Tableau de bord</h1>
<button onClick={() => setCount(count + 1)}>Compteur : {count}</button>
</div>
);
}Mode développement
Lors de la construction en local, activez le mode développement en définissant development: true dans Bun.serve().
import homepage from "./index.html";
import dashboard from "./dashboard.html";
Bun.serve({
routes: {
"/": homepage,
"/dashboard": dashboard,
},
development: true,
fetch(req) {
// ... requêtes api
},
});Fonctionnalités du mode développement
Lorsque development est true, Bun :
- Inclut l'en-tête SourceMap dans la réponse pour que les devtools puissent afficher le code source original
- Désactive la minification
- Re-bundle les assets à chaque requête vers un fichier
.html - Active le rechargement de module à chaud (sauf si
hmr: falseest défini) - Renvoie les logs console du navigateur vers le terminal
Configuration avancée du développement
Bun.serve() prend en charge le renvoi des logs console du navigateur vers le terminal.
Pour activer cela, passez console: true dans l'objet development dans Bun.serve().
import homepage from "./index.html";
Bun.serve({
// development peut aussi être un objet.
development: {
// Activer le rechargement de module à chaud
hmr: true,
// Renvoyer les logs console du navigateur vers le terminal
console: true,
},
routes: {
"/": homepage,
},
});Lorsque console: true est défini, Bun streamera les logs console du navigateur vers le terminal. Cela réutilise la connexion WebSocket existante de HMR pour envoyer les logs.
Développement vs Production
| Fonctionnalité | Développement | Production |
|---|---|---|
| Source maps | ✅ Activées | ❌ Désactivées |
| Minification | ❌ Désactivée | ✅ Activée |
| Rechargement à chaud | ✅ Activé | ❌ Désactivé |
| Bundling d'assets | 🔄 À chaque requête | 💾 En cache |
| Logs console | 🖥️ Navigateur → Terminal | ❌ Désactivés |
| Détails d'erreur | 📝 Détaillés | 🔒 Minimaux |
Mode production
Le rechargement à chaud et development: true vous aident à itérer rapidement, mais en production, votre serveur doit être aussi rapide que possible et avoir le moins de dépendances externes que possible.
Bundling Ahead of Time (Recommandé)
À partir de Bun v1.2.17, vous pouvez utiliser Bun.build ou bun build pour bundler votre application fullstack à l'avance.
bun build --target=bun --production --outdir=dist ./src/index.tsLorsque le bundler de Bun voit une importation HTML depuis du code côté serveur, il bundle les fichiers JavaScript/TypeScript/TSX/JSX et CSS référencés dans un objet manifeste que Bun.serve() peut utiliser pour servir les assets.
import { serve } from "bun";
import index from "./index.html";
serve({
routes: { "/": index },
});Bundling au moment de l'exécution
Lorsqu'ajouter une étape de construction est trop compliqué, vous pouvez définir development: false dans Bun.serve().
Cela :
- Active la mise en cache en mémoire des assets bundle. Bun bundle les assets paresseusement lors de la première requête vers un fichier
.html, et met en cache le résultat en mémoire jusqu'au redémarrage du serveur. - Active les en-têtes
Cache-ControletETag - Minifie les fichiers JavaScript/TypeScript/TSX/JSX
import { serve } from "bun";
import homepage from "./index.html";
serve({
routes: {
"/": homepage,
},
// Mode production
development: false,
});Routes API
Gestionnaires de méthode HTTP
Définissez des points de terminaison API avec des gestionnaires de méthode HTTP :
import { serve } from "bun";
serve({
routes: {
"/api/users": {
async GET(req) {
// Gérer les requêtes GET
const users = await getUsers();
return Response.json(users);
},
async POST(req) {
// Gérer les requêtes POST
const userData = await req.json();
const user = await createUser(userData);
return Response.json(user, { status: 201 });
},
async PUT(req) {
// Gérer les requêtes PUT
const userData = await req.json();
const user = await updateUser(userData);
return Response.json(user);
},
async DELETE(req) {
// Gérer les requêtes DELETE
await deleteUser(req.params.id);
return new Response(null, { status: 204 });
},
},
},
});Routes dynamiques
Utilisez des paramètres d'URL dans vos routes :
serve({
routes: {
// Paramètre unique
"/api/users/:id": async req => {
const { id } = req.params;
const user = await getUserById(id);
return Response.json(user);
},
// Paramètres multiples
"/api/users/:userId/posts/:postId": async req => {
const { userId, postId } = req.params;
const post = await getPostByUser(userId, postId);
return Response.json(post);
},
// Routes wildcard
"/api/files/*": async req => {
const filePath = req.params["*"];
const file = await getFile(filePath);
return new Response(file);
},
},
});Gestion des requêtes
serve({
routes: {
"/api/data": {
async POST(req) {
// Analyser le corps JSON
const body = await req.json();
// Accéder aux en-têtes
const auth = req.headers.get("Authorization");
// Accéder aux paramètres d'URL
const { id } = req.params;
// Accéder aux paramètres de requête
const url = new URL(req.url);
const page = url.searchParams.get("page") || "1";
// Retourner la réponse
return Response.json({
message: "Données traitées",
page: parseInt(page),
authenticated: !!auth,
});
},
},
},
});Plugins
Les plugins du bundler de Bun sont également pris en charge lors du bundling de routes statiques.
Pour configurer des plugins pour Bun.serve, ajoutez un tableau plugins dans la section [serve.static] de votre bunfig.toml.
Plugin TailwindCSS
Vous pouvez utiliser TailwindCSS en installant et en ajoutant le package tailwindcss et le plugin bun-plugin-tailwind.
bun add tailwindcss bun-plugin-tailwind[serve.static]
plugins = ["bun-plugin-tailwind"]Cela vous permettra d'utiliser les classes utilitaires TailwindCSS dans vos fichiers HTML et CSS. Tout ce que vous avez à faire est d'importer tailwindcss quelque part :
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="tailwindcss" />
</head>
<!-- le reste de votre HTML... -->
</html>Alternativement, vous pouvez importer TailwindCSS dans votre fichier CSS :
@import "tailwindcss";
.custom-class {
@apply bg-red-500 text-white;
}<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./style.css" />
</head>
<!-- le reste de votre HTML... -->
</html>Plugins personnalisés
Tout fichier ou module JS qui exporte un objet de plugin de bundler valide (essentiellement un objet avec un champ name et setup) peut être placé dans le tableau plugins :
[serve.static]
plugins = ["./my-plugin-implementation.ts"]import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "my-custom-plugin",
setup(build) {
// Implémentation du plugin
build.onLoad({ filter: /\.custom$/ }, async args => {
const text = await Bun.file(args.path).text();
return {
contents: `export default ${JSON.stringify(text)};`,
loader: "js",
};
});
},
};
export default myPlugin;Bun résoudra et chargera paresseusement chaque plugin et les utilisera pour bundler vos routes.
NOTE
Ceci est actuellement dans `bunfig.toml` pour rendre possible de savoir statiquement quels plugins sont utilisés lorsque nous intégrerons éventuellement cela avec la CLI `bun build`. Ces plugins fonctionnent dans l'API JS de `Bun.build()`, mais ne sont pas encore pris en charge dans la CLI.Variables d'environnement inline
Bun peut remplacer les références process.env.* dans votre JavaScript et TypeScript frontend par leurs valeurs réelles au moment de la construction. Configurez l'option env dans votre bunfig.toml :
[serve.static]
env = "PUBLIC_*" # uniquement les variables d'environnement commençant par PUBLIC_ (recommandé)
# env = "inline" # inline toutes les variables d'environnement
# env = "disable" # désactiver le remplacement des variables d'environnement (par défaut)Note
Cela fonctionne uniquement avec les références littérales process.env.FOO, pas import.meta.env ou l'accès indirect comme const env = process.env; env.FOO.
Si une variable d'environnement n'est pas définie, vous pouvez voir des erreurs d'exécution comme ReferenceError: process is not defined dans le navigateur.
Consultez la documentation HTML & sites statiques pour plus de détails sur la configuration au moment de la construction et des exemples.
Comment cela fonctionne
Bun utilise HTMLRewriter pour scanner les balises <script> et <link> dans les fichiers HTML, les utilise comme points d'entrée pour le bundler de Bun, génère un bundle optimisé pour les fichiers JavaScript/TypeScript/TSX/JSX et CSS, et sert le résultat.
Pipeline de traitement
1. Traitement des scripts
- Transpile TypeScript, JSX et TSX dans les balises
<script> - Bundle les dépendances importées
- Génère des sourcemaps pour le débogage
- Minifie lorsque
developmentn'est pastruedansBun.serve()
<script type="module" src="./counter.tsx"></script>2. Traitement des
- Traite les importations CSS et les balises
<link> - Concatène les fichiers CSS
- Réécrit les url et les chemins d'assets pour inclure des hachages adressables par contenu dans les URL
<link rel="stylesheet" href="./styles.css" />3. Traitement des
& assets
- Les liens vers les assets sont réécrits pour inclure des hachages adressables par contenu dans les URL
- Les petits assets dans les fichiers CSS sont inline dans des URL
data:, réduisant le nombre total de requêtes HTTP envoyées
4. Réécriture HTML
- Combine toutes les balises
<script>en une seule balise<script>avec un hachage adressable par contenu dans l'URL - Combine toutes les balises
<link>en une seule balise<link>avec un hachage adressable par contenu dans l'URL - Produit un nouveau fichier HTML
5. Service
- Tous les fichiers de sortie du bundler sont exposés comme routes statiques, utilisant le même mécanisme en interne que lorsque vous passez un objet Response à
staticdansBun.serve(). - Cela fonctionne de manière similaire à la façon dont
Bun.buildtraite les fichiers HTML.
Exemple complet
Voici un exemple complet d'application fullstack :
import { serve } from "bun";
import { Database } from "bun:sqlite";
import homepage from "./public/index.html";
import dashboard from "./public/dashboard.html";
// Initialiser la base de données
const db = new Database("app.db");
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
const server = serve({
routes: {
// Routes frontend
"/": homepage,
"/dashboard": dashboard,
// Routes API
"/api/users": {
async GET() {
const users = db.query("SELECT * FROM users").all();
return Response.json(users);
},
async POST(req) {
const { name, email } = await req.json();
try {
const result = db.query("INSERT INTO users (name, email) VALUES (?, ?) RETURNING *").get(name, email);
return Response.json(result, { status: 201 });
} catch (error) {
return Response.json({ error: "Email déjà existant" }, { status: 400 });
}
},
},
"/api/users/:id": {
async GET(req) {
const { id } = req.params;
const user = db.query("SELECT * FROM users WHERE id = ?").get(id);
if (!user) {
return Response.json({ error: "Utilisateur non trouvé" }, { status: 404 });
}
return Response.json(user);
},
async DELETE(req) {
const { id } = req.params;
const result = db.query("DELETE FROM users WHERE id = ?").run(id);
if (result.changes === 0) {
return Response.json({ error: "Utilisateur non trouvé" }, { status: 404 });
}
return new Response(null, { status: 204 });
},
},
// Point de terminaison de vérification de santé
"/api/health": {
GET() {
return Response.json({
status: "ok",
timestamp: new Date().toISOString(),
});
},
},
},
// Activer le mode développement
development: {
hmr: true,
console: true,
},
// Fallback pour les routes non correspondantes
fetch(req) {
return new Response("Non trouvé", { status: 404 });
},
});
console.log(`🚀 Serveur en cours d'exécution sur ${server.url}`);<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Application Fullstack Bun</title>
<link rel="stylesheet" href="../src/styles.css" />
</head>
<body>
<div id="root"></div>
<script type="module" src="../src/main.tsx"></script>
</body>
</html>import { createRoot } from "react-dom/client";
import { App } from "./App";
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(<App />);import { useState, useEffect } from "react";
interface User {
id: number;
name: string;
email: string;
created_at: string;
}
export function App() {
const [users, setUsers] = useState<User[]>([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const fetchUsers = async () => {
const response = await fetch("/api/users");
const data = await response.json();
setUsers(data);
};
const createUser = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name, email }),
});
if (response.ok) {
setName("");
setEmail("");
await fetchUsers();
} else {
const error = await response.json();
alert(error.error);
}
} catch (error) {
alert("Échec de la création de l'utilisateur");
} finally {
setLoading(false);
}
};
const deleteUser = async (id: number) => {
if (!confirm("Êtes-vous sûr ?")) return;
try {
const response = await fetch(`/api/users/${id}`, {
method: "DELETE",
});
if (response.ok) {
await fetchUsers();
}
} catch (error) {
alert("Échec de la suppression de l'utilisateur");
}
};
useEffect(() => {
fetchUsers();
}, []);
return (
<div className="container">
<h1>Gestion des utilisateurs</h1>
<form onSubmit={createUser} className="form">
<input type="text" placeholder="Nom" value={name} onChange={e => setName(e.target.value)} required />
<input type="email" placeholder="Email" value={email} onChange={e => setEmail(e.target.value)} required />
<button type="submit" disabled={loading}>
{loading ? "Création..." : "Créer un utilisateur"}
</button>
</form>
<div className="users">
<h2>Utilisateurs ({users.length})</h2>
{users.map(user => (
<div key={user.id} className="user-card">
<div>
<strong>{user.name}</strong>
<br />
<span>{user.email}</span>
</div>
<button onClick={() => deleteUser(user.id)} className="delete-btn">
Supprimer
</button>
</div>
))}
</div>
</div>
);
}* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #f5f5f5;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
h1 {
color: #2563eb;
margin-bottom: 2rem;
}
.form {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.form input {
flex: 1;
min-width: 200px;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.form button {
padding: 0.75rem 1.5rem;
background: #2563eb;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.form button:hover {
background: #1d4ed8;
}
.form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.users {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-card {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid #eee;
}
.user-card:last-child {
border-bottom: none;
}
.delete-btn {
padding: 0.5rem 1rem;
background: #dc2626;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.delete-btn:hover {
background: #b91c1c;
}Bonnes pratiques
Structure du projet
my-app/
├── src/
│ ├── components/
│ │ ├── Header.tsx
│ │ └── UserList.tsx
│ ├── styles/
│ │ ├── globals.css
│ │ └── components.css
│ ├── utils/
│ │ └── api.ts
│ ├── App.tsx
│ └── main.tsx
├── public/
│ ├── index.html
│ ├── dashboard.html
│ └── favicon.ico
├── server/
│ ├── routes/
│ │ ├── users.ts
│ │ └── auth.ts
│ ├── db/
│ │ └── schema.sql
│ └── index.ts
├── bunfig.toml
└── package.jsonConfiguration basée sur l'environnement
export const config = {
development: process.env.NODE_ENV !== "production",
port: process.env.PORT || 3000,
database: {
url: process.env.DATABASE_URL || "./dev.db",
},
cors: {
origin: process.env.CORS_ORIGIN || "*",
},
};Gestion des erreurs
export function errorHandler(error: Error, req: Request) {
console.error("Erreur serveur :", error);
if (process.env.NODE_ENV === "production") {
return Response.json({ error: "Erreur interne du serveur" }, { status: 500 });
}
return Response.json(
{
error: error.message,
stack: error.stack,
},
{ status: 500 },
);
}Helpers de réponse API
export function json(data: any, status = 200) {
return Response.json(data, { status });
}
export function error(message: string, status = 400) {
return Response.json({ error: message }, { status });
}
export function notFound(message = "Non trouvé") {
return error(message, 404);
}
export function unauthorized(message = "Non autorisé") {
return error(message, 401);
}Sécurité de type
export interface User {
id: number;
name: string;
email: string;
created_at: string;
}
export interface CreateUserRequest {
name: string;
email: string;
}
export interface ApiResponse<T> {
data?: T;
error?: string;
}Déploiement
Construction production
# Construire pour la production
bun build --target=bun --production --outdir=dist ./server/index.ts
# Exécuter le serveur de production
NODE_ENV=production bun dist/index.jsDéploiement Docker
FROM oven/bun:1 as base
WORKDIR /usr/src/app
# Installer les dépendances
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile
# Copier le code source
COPY . .
# Construire l'application
RUN bun build --target=bun --production --outdir=dist ./server/index.ts
# Étape de production
FROM oven/bun:1-slim
WORKDIR /usr/src/app
COPY --from=base /usr/src/app/dist ./
COPY --from=base /usr/src/app/public ./public
EXPOSE 3000
CMD ["bun", "index.js"]Variables d'environnement
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/myapp
CORS_ORIGIN=https://myapp.comMigration depuis d'autres frameworks
Depuis Express + Webpack
// Avant (Express + Webpack)
app.use(express.static("dist"));
app.get("/api/users", (req, res) => {
res.json(users);
});
// Après (Bun fullstack)
serve({
routes: {
"/": homepage, // Remplace express.static
"/api/users": {
GET() {
return Response.json(users);
},
},
},
});Depuis les routes API Next.js
// Avant (Next.js)
export default function handler(req, res) {
if (req.method === 'GET') {
res.json(users);
}
}
// Après (Bun)
"/api/users": {
GET() { return Response.json(users); }
}Limitations et plans futurs
Limitations actuelles
- L'intégration de la CLI
bun buildn'est pas encore disponible pour les applications fullstack - La découverte automatique des routes API n'est pas implémentée
- Le rendu côté serveur (SSR) n'est pas intégré
Fonctionnalités prévues
- Intégration avec la CLI
bun build - Routage basé sur les fichiers pour les points de terminaison API
- Support SSR intégré
- Écosystème de plugins amélioré
NOTE
Ceci est un travail en cours. Les fonctionnalités et les API peuvent changer au fur et à mesure que Bun continue d'évoluer.