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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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/bun

La 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.ts

Visitez 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 users

N'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.ts

Testez 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-ui

Ajoutez 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-api

Option 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 deploy

Option 3 : Cloudflare Workers

Hono fonctionne parfaitement avec Workers. Mettez à jour votre export :

export default app

Déployez avec Wrangler :

bunx wrangler deploy

Comparaison des performances

Voici pourquoi Hono + Bun surpasse Express + Node :

MétriqueExpress + NodeHono + Bun
Requêtes/sec~15 000~90 000
Temps de démarrage~300ms~25ms
Utilisation mémoire~50Mo~20Mo
Taille du bundleLourd~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


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur 15 Les Bases de Laravel 11 : Logging.

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