Créer des API REST avec Hono et Bun : L'alternative moderne à Express

Créer des API REST avec Hono et Bun
L'écosystème JavaScript a considérablement évolué ces dernières années. Si Express.js nous a bien servis pendant plus d'une décennie, des alternatives modernes offrent désormais de meilleures performances, une sécurité de typage renforcée et une expérience développeur optimisée. Dans ce tutoriel complet, nous explorerons Hono — un framework web ultra-rapide — combiné à Bun — un runtime JavaScript fulgurant.
Pourquoi Hono + Bun ?
Avant de plonger dans le code, comprenons pourquoi cette combinaison gagne en popularité :
Avantages du runtime Bun
- Vitesse : Jusqu'à 4 fois plus rapide que Node.js pour de nombreuses opérations
- TypeScript natif : Aucune transpilation nécessaire
- Bundler intégré : Plus besoin de webpack ou esbuild
- SQLite intégré : Base de données prête à l'emploi
- Compatible npm : Utilisez vos packages existants
Avantages du framework Hono
- Ultra-léger : Environ 14 Ko, zéro dépendance
- Multi-runtime : Fonctionne sur Bun, Node, Deno, Cloudflare Workers
- Typé : Support TypeScript de première classe
- API moderne : Middleware, routage et validation intégrés
- Standards Web : Utilise l'API Fetch, objets Request/Response
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Des connaissances de base en JavaScript/TypeScript
- Une familiarité avec les concepts d'API REST
- Un confort avec le terminal
- Un éditeur de code (VS Code recommandé)
Ce tutoriel utilise Bun 1.1+ et Hono 4.x. Les commandes peuvent légèrement varier avec d'autres versions.
Étape 1 : Installer Bun
Si vous n'avez pas encore installé Bun, c'est très simple :
# macOS et Linux
curl -fsSL https://bun.sh/install | bash
# Windows (avec PowerShell)
powershell -c "irm bun.sh/install.ps1 | iex"Vérifiez l'installation :
bun --version
# Devrait afficher : 1.1.x ou supérieurÉtape 2 : Créer votre projet
Créons un nouveau projet :
# Créer le répertoire du projet
mkdir hono-api && cd hono-api
# Initialiser avec Bun
bun init -y
# Installer Hono
bun add hono
# Installer les dépendances de développement
bun add -d @types/bunLa structure de votre projet devrait ressembler à :
hono-api/
├── node_modules/
├── package.json
├── tsconfig.json
├── bun.lockb
└── index.ts
Étape 3 : Votre premier serveur Hono
Remplacez le contenu de index.ts par :
import { Hono } from 'hono'
const app = new Hono()
// Route racine
app.get('/', (c) => {
return c.json({
message: 'Bienvenue sur Hono API !',
version: '1.0.0',
timestamp: new Date().toISOString()
})
})
// Point de vérification de santé
app.get('/health', (c) => {
return c.json({ status: 'healthy' })
})
// Démarrer le serveur
export default {
port: 3000,
fetch: app.fetch
}Lancez votre serveur :
bun run index.tsVisitez http://localhost:3000 — vous devriez voir la réponse JSON !
Bun prend en charge le rechargement à chaud nativement. Utilisez bun --watch index.ts pendant le développement pour des redémarrages automatiques.
Étape 4 : Structurer votre API
Pour une API prête pour la production, organisons correctement notre code. Créez cette structure de dossiers :
hono-api/
├── src/
│ ├── index.ts
│ ├── routes/
│ │ ├── index.ts
│ │ ├── users.ts
│ │ └── products.ts
│ ├── middleware/
│ │ ├── auth.ts
│ │ └── logger.ts
│ ├── services/
│ │ └── database.ts
│ └── types/
│ └── index.ts
├── package.json
└── tsconfig.json
Créez la structure :
mkdir -p src/{routes,middleware,services,types}
touch src/index.ts
touch src/routes/{index,users,products}.ts
touch src/middleware/{auth,logger}.ts
touch src/services/database.ts
touch src/types/index.tsÉtape 5 : Définir les types
Dans src/types/index.ts :
export interface User {
id: string
name: string
email: string
createdAt: Date
}
export interface Product {
id: string
name: string
price: number
description: string
stock: number
}
export interface ApiResponse<T> {
success: boolean
data?: T
error?: string
meta?: {
page?: number
total?: number
}
}Étape 6 : Créer un middleware de journalisation
Dans src/middleware/logger.ts :
import { MiddlewareHandler } from 'hono'
export const logger: MiddlewareHandler = async (c, next) => {
const start = Date.now()
const method = c.req.method
const path = c.req.path
console.log(`→ ${method} ${path}`)
await next()
const duration = Date.now() - start
const status = c.res.status
console.log(`← ${method} ${path} ${status} (${duration}ms)`)
}Étape 7 : Construire le middleware d'authentification
Dans src/middleware/auth.ts :
import { MiddlewareHandler } from 'hono'
import { HTTPException } from 'hono/http-exception'
// Authentification simple par clé API
export const authMiddleware: MiddlewareHandler = async (c, next) => {
const apiKey = c.req.header('X-API-Key')
if (!apiKey) {
throw new HTTPException(401, {
message: 'Clé API requise'
})
}
// En production, validez contre la base de données
const validKeys = ['demo-key-123', 'test-key-456']
if (!validKeys.includes(apiKey)) {
throw new HTTPException(403, {
message: 'Clé API invalide'
})
}
// Attacher les infos utilisateur au contexte
c.set('apiKey', apiKey)
await next()
}
// Exemple d'authentification JWT
export const jwtAuth: MiddlewareHandler = async (c, next) => {
const authHeader = c.req.header('Authorization')
if (!authHeader?.startsWith('Bearer ')) {
throw new HTTPException(401, {
message: 'Token Bearer requis'
})
}
const token = authHeader.slice(7)
try {
// En production, vérifiez le JWT correctement
// const payload = await verifyJwt(token)
// c.set('user', payload)
await next()
} catch {
throw new HTTPException(401, {
message: 'Token invalide'
})
}
}Étape 8 : Configurer le service de base de données
Nous utilisons SQLite intégré à Bun pour simplifier. Dans src/services/database.ts :
import { Database } from 'bun:sqlite'
import type { User, Product } from '../types'
// Initialiser la base de données
const db = new Database('app.db')
// Créer les tables
db.run(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`)
db.run(`
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL,
description TEXT,
stock INTEGER DEFAULT 0
)
`)
// Opérations utilisateurs
export const userService = {
findAll(): User[] {
return db.query('SELECT * FROM users').all() as User[]
},
findById(id: string): User | null {
return db.query('SELECT * FROM users WHERE id = ?').get(id) as User | null
},
create(user: Omit<User, 'createdAt'>): User {
const stmt = db.prepare(
'INSERT INTO users (id, name, email) VALUES (?, ?, ?)'
)
stmt.run(user.id, user.name, user.email)
return this.findById(user.id)!
},
update(id: string, data: Partial<User>): User | null {
const fields = Object.keys(data)
.map(k => `${k} = ?`)
.join(', ')
const values = Object.values(data)
db.prepare(`UPDATE users SET ${fields} WHERE id = ?`).run(...values, id)
return this.findById(id)
},
delete(id: string): boolean {
const result = db.prepare('DELETE FROM users WHERE id = ?').run(id)
return result.changes > 0
}
}
// Opérations produits
export const productService = {
findAll(): Product[] {
return db.query('SELECT * FROM products').all() as Product[]
},
findById(id: string): Product | null {
return db.query('SELECT * FROM products WHERE id = ?').get(id) as Product | null
},
create(product: Product): Product {
const stmt = db.prepare(
'INSERT INTO products (id, name, price, description, stock) VALUES (?, ?, ?, ?, ?)'
)
stmt.run(product.id, product.name, product.price, product.description, product.stock)
return this.findById(product.id)!
},
update(id: string, data: Partial<Product>): Product | null {
const fields = Object.keys(data)
.map(k => `${k} = ?`)
.join(', ')
const values = Object.values(data)
db.prepare(`UPDATE products SET ${fields} WHERE id = ?`).run(...values, id)
return this.findById(id)
},
delete(id: string): boolean {
const result = db.prepare('DELETE FROM products WHERE id = ?').run(id)
return result.changes > 0
}
}
export { db }Étape 9 : Créer les routes utilisateurs
Dans src/routes/users.ts :
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { userService } from '../services/database'
import type { ApiResponse, User } from '../types'
const users = new Hono()
// Schémas de validation
const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email()
})
const updateUserSchema = z.object({
name: z.string().min(2).max(100).optional(),
email: z.string().email().optional()
})
// GET /users - Lister tous les utilisateurs
users.get('/', (c) => {
const allUsers = userService.findAll()
const response: ApiResponse<User[]> = {
success: true,
data: allUsers,
meta: { total: allUsers.length }
}
return c.json(response)
})
// GET /users/:id - Obtenir un utilisateur
users.get('/:id', (c) => {
const id = c.req.param('id')
const user = userService.findById(id)
if (!user) {
return c.json<ApiResponse<null>>({
success: false,
error: 'Utilisateur non trouvé'
}, 404)
}
return c.json<ApiResponse<User>>({
success: true,
data: user
})
})
// POST /users - Créer un utilisateur
users.post(
'/',
zValidator('json', createUserSchema),
(c) => {
const body = c.req.valid('json')
const id = crypto.randomUUID()
try {
const user = userService.create({ id, ...body })
return c.json<ApiResponse<User>>({
success: true,
data: user
}, 201)
} catch (error) {
return c.json<ApiResponse<null>>({
success: false,
error: 'Cet email existe déjà'
}, 409)
}
}
)
// PUT /users/:id - Mettre à jour un utilisateur
users.put(
'/:id',
zValidator('json', updateUserSchema),
(c) => {
const id = c.req.param('id')
const body = c.req.valid('json')
const user = userService.update(id, body)
if (!user) {
return c.json<ApiResponse<null>>({
success: false,
error: 'Utilisateur non trouvé'
}, 404)
}
return c.json<ApiResponse<User>>({
success: true,
data: user
})
}
)
// DELETE /users/:id - Supprimer un utilisateur
users.delete('/:id', (c) => {
const id = c.req.param('id')
const deleted = userService.delete(id)
if (!deleted) {
return c.json<ApiResponse<null>>({
success: false,
error: 'Utilisateur non trouvé'
}, 404)
}
return c.json<ApiResponse<null>>({
success: true
}, 204)
})
export default usersN'oubliez pas d'installer Zod pour la validation :
bun add zod @hono/zod-validatorÉtape 10 : Créer les routes produits
Dans src/routes/products.ts :
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { productService } from '../services/database'
import type { ApiResponse, Product } from '../types'
const products = new Hono()
const productSchema = z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
description: z.string().optional(),
stock: z.number().int().min(0).default(0)
})
// GET /products
products.get('/', (c) => {
const query = c.req.query('search')
let allProducts = productService.findAll()
if (query) {
allProducts = allProducts.filter(p =>
p.name.toLowerCase().includes(query.toLowerCase())
)
}
return c.json<ApiResponse<Product[]>>({
success: true,
data: allProducts,
meta: { total: allProducts.length }
})
})
// GET /products/:id
products.get('/:id', (c) => {
const product = productService.findById(c.req.param('id'))
if (!product) {
return c.json<ApiResponse<null>>({
success: false,
error: 'Produit non trouvé'
}, 404)
}
return c.json<ApiResponse<Product>>({
success: true,
data: product
})
})
// POST /products
products.post(
'/',
zValidator('json', productSchema),
(c) => {
const body = c.req.valid('json')
const id = crypto.randomUUID()
const product = productService.create({
id,
name: body.name,
price: body.price,
description: body.description || '',
stock: body.stock
})
return c.json<ApiResponse<Product>>({
success: true,
data: product
}, 201)
}
)
// PUT /products/:id
products.put(
'/:id',
zValidator('json', productSchema.partial()),
(c) => {
const id = c.req.param('id')
const body = c.req.valid('json')
const product = productService.update(id, body)
if (!product) {
return c.json<ApiResponse<null>>({
success: false,
error: 'Produit non trouvé'
}, 404)
}
return c.json<ApiResponse<Product>>({
success: true,
data: product
})
}
)
// DELETE /products/:id
products.delete('/:id', (c) => {
const deleted = productService.delete(c.req.param('id'))
if (!deleted) {
return c.json<ApiResponse<null>>({
success: false,
error: 'Produit non trouvé'
}, 404)
}
return c.json({ success: true }, 204)
})
export default productsÉtape 11 : Combiner les routes
Dans src/routes/index.ts :
import { Hono } from 'hono'
import users from './users'
import products from './products'
const routes = new Hono()
routes.route('/users', users)
routes.route('/products', products)
export default routesÉtape 12 : Point d'entrée principal de l'application
Mettez à jour src/index.ts :
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { prettyJSON } from 'hono/pretty-json'
import { HTTPException } from 'hono/http-exception'
import { logger } from './middleware/logger'
import { authMiddleware } from './middleware/auth'
import routes from './routes'
const app = new Hono()
// Middleware global
app.use('*', logger)
app.use('*', cors({
origin: ['http://localhost:3000', 'https://votredomaine.com'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'X-API-Key']
}))
app.use('*', prettyJSON())
// Routes publiques
app.get('/', (c) => {
return c.json({
name: 'Hono REST API',
version: '1.0.0',
docs: '/docs',
health: '/health'
})
})
app.get('/health', (c) => c.json({ status: 'ok' }))
// Routes API protégées
app.use('/api/*', authMiddleware)
app.route('/api', routes)
// Gestionnaire d'erreurs global
app.onError((err, c) => {
console.error('Erreur :', err)
if (err instanceof HTTPException) {
return c.json({
success: false,
error: err.message
}, err.status)
}
return c.json({
success: false,
error: 'Erreur interne du serveur'
}, 500)
})
// Gestionnaire 404
app.notFound((c) => {
return c.json({
success: false,
error: 'Route non trouvée'
}, 404)
})
// Export pour Bun
export default {
port: process.env.PORT || 3000,
fetch: app.fetch
}
console.log('🚀 Serveur en cours d\'exécution sur http://localhost:3000')Étape 13 : Tester votre API
Démarrez le serveur :
bun run src/index.tsTestez avec curl :
# Vérification de santé
curl http://localhost:3000/health
# Créer un utilisateur (avec clé API)
curl -X POST http://localhost:3000/api/users \
-H "Content-Type: application/json" \
-H "X-API-Key: demo-key-123" \
-d '{"name": "Jean Dupont", "email": "jean@exemple.fr"}'
# Lister les utilisateurs
curl http://localhost:3000/api/users \
-H "X-API-Key: demo-key-123"
# Créer un produit
curl -X POST http://localhost:3000/api/products \
-H "Content-Type: application/json" \
-H "X-API-Key: demo-key-123" \
-d '{"name": "Ordinateur portable", "price": 999.99, "stock": 50}'Ne commitez jamais de clés API dans le contrôle de version. Utilisez des variables d'environnement pour les données sensibles en production.
Étape 14 : Ajouter la documentation OpenAPI
Hono supporte OpenAPI/Swagger. Installez le package :
bun add @hono/swagger-uiAjoutez le point de documentation à src/index.ts :
import { swaggerUI } from '@hono/swagger-ui'
// Ajouter après les autres routes
app.get('/docs', swaggerUI({ url: '/openapi.json' }))
app.get('/openapi.json', (c) => {
return c.json({
openapi: '3.0.0',
info: {
title: 'Hono REST API',
version: '1.0.0',
description: 'Une API REST moderne construite avec Hono et Bun'
},
servers: [
{ url: 'http://localhost:3000', description: 'Développement' }
],
paths: {
'/api/users': {
get: {
summary: 'Lister tous les utilisateurs',
security: [{ apiKey: [] }],
responses: {
'200': { description: 'Liste des utilisateurs' }
}
}
}
// Ajouter plus de chemins...
},
components: {
securitySchemes: {
apiKey: {
type: 'apiKey',
in: 'header',
name: 'X-API-Key'
}
}
}
})
})Étape 15 : Déploiement
Option 1 : Docker
Créez un Dockerfile :
FROM oven/bun:1.1-alpine
WORKDIR /app
COPY package.json bun.lockb ./
RUN bun install --frozen-lockfile --production
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["bun", "run", "src/index.ts"]Construisez et exécutez :
docker build -t hono-api .
docker run -p 3000:3000 hono-apiOption 2 : Fly.io
Créez fly.toml :
app = "votre-hono-api"
primary_region = "fra"
[build]
builder = "oven/bun:1.1"
[http_service]
internal_port = 3000
force_https = true
[env]
NODE_ENV = "production"Déployez :
fly launch
fly deployOption 3 : Cloudflare Workers
Hono fonctionne parfaitement avec Workers. Mettez à jour votre export :
export default appDéployez avec Wrangler :
bunx wrangler deployComparaison des performances
Voici pourquoi Hono + Bun surpasse Express + Node :
| Métrique | Express + Node | Hono + Bun |
|---|---|---|
| Requêtes/sec | ~15 000 | ~90 000 |
| Temps de démarrage | ~300ms | ~25ms |
| Utilisation mémoire | ~50Mo | ~20Mo |
| Taille du bundle | Lourd | ~14Ko |
Résumé
Vous avez construit une API REST complète avec :
- ✅ Le framework Hono pour le routage et les middlewares
- ✅ Le runtime Bun pour des performances maximales
- ✅ TypeScript pour la sécurité de typage
- ✅ Base de données SQLite avec opérations CRUD
- ✅ Validation des entrées avec Zod
- ✅ Middleware d'authentification
- ✅ CORS et gestion des erreurs
- ✅ Documentation OpenAPI
- ✅ Options de déploiement
Prochaines étapes
- Ajouter la limitation de débit avec
hono/rate-limit - Implémenter l'authentification JWT complète
- Ajouter les migrations de base de données
- Configurer un pipeline CI/CD
- Ajouter des tests unitaires et d'intégration
La combinaison de Hono et Bun représente l'avenir du développement backend JavaScript — rapide, typé et agréable pour les développeurs. Lancez-vous !
Ressources
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

AI SDK 4.0 : Nouvelles Fonctionnalites et Cas d'Utilisation
Decouvrez les nouvelles fonctionnalites et cas d'utilisation d'AI SDK 4.0, incluant le support PDF, l'utilisation de l'ordinateur et plus encore.

Introduction au Model Context Protocol (MCP)
Découvrez le Model Context Protocol (MCP), ses cas d'usage, ses avantages et comment construire et utiliser un serveur MCP avec TypeScript.

Flouci : Le Compte Bancaire Professionnel Gratuit pour Auto-entrepreneurs en Tunisie
Découvrez comment Flouci offre un compte bancaire professionnel gratuit et entièrement digital aux auto-entrepreneurs et professionnels (personne physique) en Tunisie, simplifiant la gestion financière.