ITSkillsCenter
Développement Web

Bun + Hono : construire une API REST type-safe en 2026

8 دقائق للقراءة

Construire une API REST en 2026 ne se résume plus à choisir Express + Node. Le combo Bun + Hono offre une expérience moderne, type-safe, ultra-rapide et portable : la même API peut tourner sur Bun, Node.js, Deno, Cloudflare Workers, AWS Lambda, ou n’importe quel runtime JavaScript moderne. Avec quelques lignes de code, vous obtenez du routing avancé, de la validation Zod, du middleware modulaire et des performances qui dépassent largement Express. Voici le tutoriel complet, du hello world à la mise en production.

Ce tutoriel s’inscrit dans notre série Bun. Pour les bases (installation, philosophie, comparaison avec Node.js), lisez d’abord notre guide complet Bun en production 2026.

Pourquoi Hono ?

Hono est un framework web ultra-léger (moins de 14 Ko gzippé) créé par Yusuke Wada. Il s’inspire d’Express en termes d’API mais est entièrement réécrit en TypeScript moderne avec une attention particulière à la performance et au type-safety. En 2026, Hono est devenu le choix par défaut pour les API JavaScript modernes, notamment pour ces raisons :

  • Portable : votre code tourne identiquement sur Bun, Node.js, Deno, Cloudflare Workers, Vercel Edge, AWS Lambda. Pas de vendor lock-in.
  • Type-safe : les routes, paramètres, body et réponses sont typés. Si vous changez un schéma, TypeScript signale toutes les utilisations affectées.
  • Performance : sur Bun, Hono atteint 30 000-50 000 RPS sur du hardware modeste, soit 5 à 10x Express.
  • Middleware riche : auth JWT, CORS, compression, rate limiting, logger structuré, cache, tous officiels et maintenus.
  • Validation Zod intégrée via @hono/zod-validator
  • RPC client : générer un client TypeScript type-safe à partir des routes server, sans codegen, en quelques lignes.

Prérequis

  • Bun installé (voir guide Bun)
  • Connaissance basique de TypeScript et des API REST
  • Un éditeur avec support TypeScript (VS Code recommandé)
  • Niveau attendu : intermédiaire
  • Temps : 30 minutes pour le hello world, 2 heures pour une API CRUD complète avec auth

Étape 1 — Initialiser le projet

mkdir mon-api && cd mon-api
bun init -y

# Installer Hono et Zod
bun add hono zod @hono/zod-validator
bun add -D @types/bun

Le fichier tsconfig.json par défaut généré par Bun convient. Si vous voulez de la stricte ultra-stricte, ajoutez "strict": true, "noUncheckedIndexedAccess": true.

Étape 2 — Premier serveur Hono

// src/index.ts
import { Hono } from "hono";
import { logger } from "hono/logger";
import { cors } from "hono/cors";
import { secureHeaders } from "hono/secure-headers";

const app = new Hono();

app.use("*", logger());
app.use("*", secureHeaders());
app.use("/api/*", cors({
  origin: ["https://app.exemple.sn", "http://localhost:3000"],
  credentials: true,
}));

app.get("/", (c) => c.text("Hono API on Bun"));

app.get("/api/health", (c) => c.json({
  status: "ok",
  runtime: "bun",
  time: new Date().toISOString(),
}));

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

Lancez avec bun run --hot src/index.ts. Le flag --hot active le hot reload : modifiez le code, le serveur redémarre instantanément, pas besoin de nodemon ou tsx.

Étape 3 — Routing CRUD avec validation

// src/routes/users.ts
import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const users = new Hono();

const createUserSchema = z.object({
  name: z.string().min(2).max(100),
  email: z.string().email(),
  age: z.number().int().min(13).max(120),
});

const userIdParam = z.object({
  id: z.string().regex(/^\d+$/).transform(Number),
});

// In-memory store (en prod : remplacer par Drizzle/Postgres)
let userStore: Array<{ id: number; name: string; email: string; age: number }> = [];
let nextId = 1;

users.get("/", (c) => c.json({ data: userStore, total: userStore.length }));

users.post("/", zValidator("json", createUserSchema), (c) => {
  const body = c.req.valid("json");
  const user = { id: nextId++, ...body };
  userStore.push(user);
  return c.json(user, 201);
});

users.get("/:id", zValidator("param", userIdParam), (c) => {
  const { id } = c.req.valid("param");
  const user = userStore.find((u) => u.id === id);
  if (!user) return c.json({ error: "Not found" }, 404);
  return c.json(user);
});

users.delete("/:id", zValidator("param", userIdParam), (c) => {
  const { id } = c.req.valid("param");
  const idx = userStore.findIndex((u) => u.id === id);
  if (idx === -1) return c.json({ error: "Not found" }, 404);
  userStore.splice(idx, 1);
  return c.json({ deleted: true });
});

export default users;

Brancher dans index.ts :

import users from "./routes/users";
app.route("/api/users", users);

Testez avec curl :

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Aïssatou","email":"a@exemple.sn","age":28}'

Si vous envoyez un body invalide (email malformé par exemple), Hono renvoie automatiquement un 400 avec le détail Zod. Pas de code de validation à écrire.

Étape 4 — Authentification JWT

import { jwt, sign } from "hono/jwt";

const JWT_SECRET = process.env.JWT_SECRET ?? (() => {
  throw new Error("JWT_SECRET manquant");
})();

// Route publique pour login
app.post("/api/auth/login", async (c) => {
  const { email, password } = await c.req.json();

  // Vérifier credentials (en prod : DB + bcrypt)
  if (email !== "demo@exemple.sn" || password !== "demo") {
    return c.json({ error: "Invalid credentials" }, 401);
  }

  const token = await sign(
    { sub: email, exp: Math.floor(Date.now() / 1000) + 3600 },
    JWT_SECRET,
  );

  return c.json({ token });
});

// Middleware JWT pour les routes protégées
app.use("/api/private/*", jwt({ secret: JWT_SECRET }));

app.get("/api/private/me", (c) => {
  const payload = c.get("jwtPayload");
  return c.json({ user: payload });
});

Le middleware jwt() de Hono extrait le token du header Authorization: Bearer ..., le vérifie, et expose le payload via c.get("jwtPayload"). Si le token manque ou est invalide, 401 automatique.

Étape 5 — Connecter une base de données

L’in-memory store sert pour le prototype. En prod, utilisez PostgreSQL avec Drizzle ORM. Voir notre tutoriel Bun + Drizzle. Architecture typique :

// src/db/schema.ts (Drizzle)
import { pgTable, serial, varchar, integer } from "drizzle-orm/pg-core";

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: varchar("name", { length: 100 }).notNull(),
  email: varchar("email", { length: 255 }).notNull().unique(),
  age: integer("age").notNull(),
});

// src/db/client.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";

const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client);

Et dans la route :

import { db } from "../db/client";
import { users } from "../db/schema";
import { eq } from "drizzle-orm";

usersRoute.get("/:id", zValidator("param", userIdParam), async (c) => {
  const { id } = c.req.valid("param");
  const result = await db.select().from(users).where(eq(users.id, id));
  if (!result.length) return c.json({ error: "Not found" }, 404);
  return c.json(result[0]);
});

Étape 6 — Tests

// src/routes/users.test.ts
import { describe, it, expect } from "bun:test";
import app from "../index";

describe("Users API", () => {
  it("GET /api/users returns array", async () => {
    const res = await app.request("/api/users");
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(Array.isArray(body.data)).toBe(true);
  });

  it("POST /api/users validates input", async () => {
    const res = await app.request("/api/users", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ name: "X" }), // age et email manquants
    });
    expect(res.status).toBe(400);
  });
});

Lancez avec bun test. Pas de mock HTTP server à créer : Hono a une méthode app.request() qui simule des requêtes directement contre l’app, sans démarrer de serveur.

Étape 7 — Client RPC type-safe

L’une des killer features de Hono : générer un client TypeScript depuis vos routes, sans codegen :

// src/index.ts (côté server)
const route = app
  .get("/api/users", (c) => c.json({ data: userStore }))
  .post("/api/users", zValidator("json", createUserSchema), (c) => {
    const body = c.req.valid("json");
    return c.json({ id: 1, ...body }, 201);
  });

export type AppType = typeof route;

// src/client.ts (côté client / front)
import { hc } from "hono/client";
import type { AppType } from "./index";

const client = hc<AppType>("https://api.exemple.sn");

const res = await client.api.users.$post({
  json: { name: "X", email: "x@y.sn", age: 30 },
});
const user = await res.json();
// user est typé automatiquement

Plus de désynchro entre back et front. Si vous changez le schéma Zod côté serveur, le front compile en erreur jusqu’à ce que vous le mettiez à jour.

Étape 8 — Déployer

Sur Coolify avec Nixpacks : poussez sur Git, Coolify détecte Bun, lance bun install && bun run start. Sur un VPS nu : voir notre tutoriel systemd / PM2. Sur Cloudflare Workers : changez l’export pour export default app et déployez via Wrangler — ça marche sans modification supplémentaire.

Adaptation Afrique de l’Ouest

Pour une PME ouest-africaine qui construit une API métier (gestion de stock, facturation Wave/OM, application interne), Bun + Hono permet de tenir 5 000-10 000 utilisateurs concurrents sur un VPS Hetzner CX22 (4 € par mois). En face, un Express classique demanderait un VPS double pour le même service. L’économie annuelle est de 50-100 € par projet.

Erreurs fréquentes

ErreurCauseSolution
CORS bloqué côté frontendOrigin manquante dans le middleware corsAjouter le domaine front dans origin: []
JWT 401 systématiqueJWT_SECRET différent entre login et middlewareCentraliser la lecture de la variable
Validation Zod ne coerce pas les typesParam URL toujours stringUtiliser .transform(Number) ou .coerce.number()
RPC client n’a pas les typesRoutes pas chaînéesChaîner .get().post() sur la même variable
app.request() retourne 404 en testPréfixe de path manquantInclure le préfixe complet dans request()

Pour aller plus loin

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é