ITSkillsCenter
Développement Web

Déployer Hono sur Cloudflare Workers : edge global pour l’Afrique 2026

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

📍 Article principal : Hono framework TypeScript en production 2026

Introduction

Une plateforme de paiement marchand utilisée à Lomé, Cotonou et Niamey servait son API depuis un VPS Hetzner FRA1. Latence moyenne mesurée depuis un terminal Itel à Cotonou : 165 ms, avec des pics à 320 ms en heure de pointe. La même API redéployée sur Cloudflare Workers est répondue par le PoP de Lagos en 38 ms p50, 95 ms p99. Pour les marchands qui scannent un QR code Wave et attendent la confirmation, le passage de 165 à 38 ms transforme l’expérience. Ce tutoriel décrit le déploiement complet d’une API Hono sur Cloudflare Workers, les bindings KV/R2/D1, la gestion des secrets, les pièges du runtime contraint, et le monitoring. À la fin, vous avez une API edge servant l’Afrique de l’Ouest avec un plan gratuit Cloudflare suffisant pour démarrer (100 000 requêtes/jour).

Prérequis

  • Compte Cloudflare gratuit (nécessite carte bancaire pour vérification mais aucun débit pour le plan Free)
  • Node 22 LTS
  • Domaine sur Cloudflare ou prêt à y être transféré
  • Connaissance de base Hono (voir le pilier)
  • Niveau : intermédiaire — Temps : 1 h 30

Étape 1 — Créer le projet Hono pour Workers

npm create hono@latest mon-api
# Choisir : cloudflare-workers
cd mon-api
npm install

Le scaffold génère un wrangler.toml avec la configuration de base, un src/index.ts avec un endpoint GET /, et les types Cloudflare Workers. Tester localement avec le runtime workerd (compatible production) :

npm run dev
# Local : http://localhost:8787

Le runtime local utilise le même moteur que la production Cloudflare. Aucune surprise lors du déploiement, contrairement à l’écart classique entre nodemon local et serveur cloud.

Étape 2 — Configurer wrangler.toml

name = "mon-api"
main = "src/index.ts"
compatibility_date = "2026-01-01"
compatibility_flags = ["nodejs_compat"]

[env.production]
routes = [{ pattern = "api.example.sn/*", zone_name = "example.sn" }]

[env.staging]
routes = [{ pattern = "api-staging.example.sn/*", zone_name = "example.sn" }]

[[kv_namespaces]]
binding = "CACHE"
id = "abc123..."  # créé via : npx wrangler kv namespace create CACHE

[[r2_buckets]]
binding = "MEDIAS"
bucket_name = "mon-api-medias"

[[d1_databases]]
binding = "DB"
database_name = "mon-api-db"
database_id = "xyz789..."

[vars]
APP_ENV = "production"

Trois bindings essentiels : KV pour le cache distribué (lecture rapide globale, écriture éventuellement consistente), R2 pour le stockage objet S3-compatible sans frais d’egress, D1 pour la base SQLite distribuée.

Étape 3 — Cache KV pour les requêtes lentes

KV brille pour les données peu changeantes mais lues souvent : configuration, taux de change, catalogue produit en lecture. Lecture en moins de 10 ms partout dans le monde grâce à la réplication multi-PoP.

type Bindings = { CACHE: KVNamespace; DB: D1Database };
const app = new Hono<{ Bindings: Bindings }>();

app.get('/api/produits', async (c) => {
  const cached = await c.env.CACHE.get('produits-actifs');
  if (cached) return c.json(JSON.parse(cached));

  const produits = await c.env.DB.prepare('SELECT * FROM produits WHERE actif=1').all();
  await c.env.CACHE.put('produits-actifs', JSON.stringify(produits.results), { expirationTtl: 300 });
  return c.json(produits.results);
});

Pour invalider le cache après une mutation, on appelle c.env.CACHE.delete('produits-actifs') dans l’endpoint POST/PATCH. Pour les invalidations multi-clés, on préfixe les clés et on utilise list({ prefix: 'produits:' }) + suppression en boucle.

Étape 4 — Stocker les images dans R2

R2 est l’équivalent S3 de Cloudflare avec un avantage majeur : aucun frais d’egress, contrairement à AWS S3 qui facture chaque sortie. Pour une plateforme média qui sert des photos à toute l’Afrique de l’Ouest, l’économie atteint plusieurs centaines d’euros par mois dès qu’on dépasse quelques To/mois.

app.post('/api/upload', async (c) => {
  const formData = await c.req.parseBody();
  const file = formData['photo'] as File;
  if (!file || file.size > 5_000_000) return c.json({ erreur: 'Fichier > 5 Mo' }, 400);

  const key = `photos/${crypto.randomUUID()}-${file.name}`;
  await c.env.MEDIAS.put(key, await file.arrayBuffer(), {
    httpMetadata: { contentType: file.type }
  });
  return c.json({ url: `https://medias.example.sn/${key}` }, 201);
});

app.get('/medias/*', async (c) => {
  const key = c.req.path.replace('/medias/', '');
  const obj = await c.env.MEDIAS.get(key);
  if (!obj) return c.notFound();
  return new Response(obj.body, { headers: { 'content-type': obj.httpMetadata?.contentType || 'application/octet-stream', 'cache-control': 'public, max-age=31536000' } });
});

Pour servir les médias publics, on configure un domaine personnalisé sur le bucket R2 (interface Cloudflare). Le CDN Cloudflare cache les réponses et le coût tend vers zéro pour le trafic en lecture.

Étape 5 — Base SQLite distribuée avec D1

D1 est une base SQLite gérée par Cloudflare, répliquée en lecture sur chaque PoP. Elle convient aux applications qui font surtout des lectures et peu d’écritures concurrentes — typiquement un blog, un catalogue, un dashboard analytique. Pour une fintech avec écritures lourdes simultanées, PostgreSQL via Neon ou Hyperdrive reste préférable.

# Créer la base et la migration
npx wrangler d1 create mon-api-db
npx wrangler d1 execute mon-api-db --command "CREATE TABLE produits (id TEXT PRIMARY KEY, nom TEXT, prix INTEGER, actif INTEGER);"
npx wrangler d1 execute mon-api-db --command "INSERT INTO produits VALUES ('p1','Tecno Camon 22',195000,1);"
app.get('/api/produits/:id', async (c) => {
  const id = c.req.param('id');
  const produit = await c.env.DB.prepare('SELECT * FROM produits WHERE id=?').bind(id).first();
  if (!produit) return c.notFound();
  return c.json(produit);
});

Pour les schémas complexes, Drizzle ORM supporte D1 nativement et offre le typage strict habituel. Voir la documentation Drizzle dialect better-sqlite3 en mode workers.

Étape 6 — Gérer les secrets

Les secrets ne se mettent jamais dans wrangler.toml ni dans le code. On les pousse via la CLI :

npx wrangler secret put JWT_SECRET
# (entrer la valeur quand demandé)
npx wrangler secret put WAVE_API_KEY
npx wrangler secret put DATABASE_URL

Côté handler, on accède via c.env.JWT_SECRET. Aucun secret ne fuit dans les logs, et la rotation se fait sans redéploiement de code. Pour les environnements multi-tenant ou multi-clients, on préfixe : WAVE_API_KEY_CLIENT_A, WAVE_API_KEY_CLIENT_B.

Étape 7 — Connaître les limites du runtime

Cloudflare Workers a des contraintes spécifiques qu’il faut connaître avant de migrer une API existante. 1 Mo de bundle en plan Free, 10 Mo en plan Paid (5 $/mois) : exclut sharp, puppeteer, ffmpeg natifs. Solution : pour le redimensionnement d’image, utiliser Cloudflare Image Resizing (transformation à la volée par le CDN). Pour le PDF, déléguer à un microservice Hetzner. 30 secondes de CPU max par requête en plan Paid : exclut les calculs lourds. Solution : Workers AI ou Durable Objects pour les tâches longues. Pas de fs natif : tout passe par R2, KV, D1 ou Hyperdrive. Modules npm Node-only : marquer nodejs_compat dans wrangler.toml couvre la majorité des cas, mais certains modules (comme net, tls bas niveau) restent inutilisables.

Pour vérifier la compatibilité d’une dépendance, lancer npm run dev en local et tester chaque endpoint. L’erreur « X is not implemented » apparaît immédiatement, contrairement à Lambda où l’erreur ne survient qu’au runtime. Cette boucle de feedback courte est un avantage majeur de Workers pour le développement.

Étape 8 — Déploiement multi-environnement

# Staging
npx wrangler deploy --env staging

# Production
npx wrangler deploy --env production

Le déploiement prend 5 à 10 secondes et propage globalement en moins d’une minute. Pour la CI GitHub Actions, on utilise l’action officielle :

- name: Deploy
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    command: deploy --env production
    workingDirectory: api

Étape 9 — Monitoring et logs

Cloudflare expose les Worker Analytics gratuitement : nombre de requêtes, latence p50/p95/p99, erreurs par PoP. Pour aller plus loin, on active « Logpush » qui envoie chaque requête vers un destinataire (R2, S3, Datadog). Coût : 0,05 $ par million de requêtes — négligeable.

Pour les erreurs applicatives, intégrer Sentry via leur SDK Workers compatible (@sentry/cloudflare) capture les exceptions avec stack trace lisible. Coût : gratuit jusqu’à 5 000 événements/mois sur le plan Sentry Developer.

Erreurs fréquentes

Erreur Cause Solution
« Script too large » Bundle > 1 Mo Auditer avec npx wrangler whoami + bundle analyzer, retirer dépendances
« X is not defined » en prod uniquement Code utilise une API Node non polyfillée Activer nodejs_compat ou remplacer par API web standard
Latence inattendue depuis Dakar Premier hit cold sur un nouveau PoP Ignorer le premier hit dans les benchmarks ou utiliser warmup
D1 query lente Pas d’index sur la colonne filtrée Créer l’index, vérifier le plan d’exécution
KV lecture inconsistante après écriture KV est éventuellement consistent Pour cohérence forte, utiliser D1 ou Durable Objects
« Too many subrequests » Plus de 50 fetch externes par requête Batcher, mettre en cache, ou refactor

Adaptation au contexte ouest-africain

Cloudflare a un PoP majeur à Lagos et un autre à Accra. Pour une audience principalement à Dakar, Abidjan, Bamako, Ouagadougou, le routing va le plus souvent vers Marseille ou Madrid (latence ~95 ms) avec parfois Lagos (latence ~50 ms). Le plan Free traite 100 000 requêtes par jour — largement suffisant pour un MVP avec quelques milliers d’utilisateurs actifs. Au-delà, le plan Paid à 5 $/mois autorise 10 millions de requêtes/mois et active des fonctionnalités avancées (Cron Triggers, Tail Workers, plus gros bundle).

Pour les marchands au Nigeria et au Ghana, Workers offre une expérience comparable à un cloud local, ce qui est rare. Pour les apps qui mélangent base PostgreSQL Hetzner et edge, Hyperdrive (cache côté Cloudflare des connexions PostgreSQL) ramène la latence db de 100 ms à 5 ms — gain énorme sur les API très requêtantes.

Tutoriels frères

Pour aller plus loin

FAQ

Workers vs Pages : quelle différence ?
Pages cible les frontends statiques avec functions optionnelles. Workers cible les API et backends. Pour une API Hono pure, on utilise Workers.

Comment connecter à PostgreSQL Hetzner depuis Workers ?
Hyperdrive de Cloudflare cache les connexions et réduit la latence à 5-10 ms. Sans Hyperdrive, postgres-js fonctionne mais avec une latence de 100-150 ms par requête.

Le plan Free suffit-il pour un produit en lancement ?
Oui jusqu’à environ 3 000 utilisateurs actifs/jour. Au-delà, le plan Paid à 5 $/mois est largement suffisant pour 50 000 utilisateurs actifs/jour.

Peut-on faire du WebSocket sur Workers ?
Oui via les Durable Objects. Plus complexe à mettre en place qu’un serveur Node classique mais scale infiniment. Pour un MVP simple, héberger le WebSocket sur un VPS Hetzner et le reste sur Workers.

Stratégie de coût et budgets

Le plan Free Cloudflare Workers couvre la majorité des MVP : 100 000 requêtes par jour, 10 ms de CPU par requête, 1 Mo de bundle, KV avec 100 000 lectures et 1 000 écritures par jour, R2 avec 10 Go de stockage gratuit. Concrètement, un SaaS B2B servant 2 000 utilisateurs actifs par jour avec 30 requêtes par session reste sous le seuil. Le passage au plan Workers Paid (5 $/mois) débloque 10 millions de requêtes par mois, KV illimité dans des bornes raisonnables, R2 toujours gratuit en egress, et active des fonctionnalités avancées comme les Cron Triggers, Tail Workers (logs streaming) et Workers AI à la demande.

Pour une fintech ouest-africaine avec un volume de 5 à 10 millions de requêtes par mois, le coût Cloudflare se situe entre 5 et 15 $/mois selon les bindings utilisés. Comparé à un cluster Kubernetes équivalent qui coûterait 200 à 500 $/mois sur AWS ou GCP, l’économie est massive. Le tradeoff : on accepte les contraintes du runtime (1 Mo, pas de fs, CPU limité) en échange d’un coût quasi nul jusqu’à un volume sérieux. Pour les charges spécifiques qui ne tiennent pas dans Workers (OCR de pièces d’identité, génération de PDF, calculs ML), on garde un microservice Hetzner accessible via fetch interne, et on garde tout le reste en edge.

Architecture hybride Workers + VPS

L’architecture la plus robuste pour 2026 combine Cloudflare Workers en frontal et un VPS Hetzner pour les charges lourdes. Le Worker reçoit toutes les requêtes, sert les endpoints rapides en edge (catalogue, profil, autocomplete), et délègue au VPS pour les opérations qui nécessitent l’écosystème Node complet (génération PDF de facture, traitement d’images, jobs longs). Cette architecture donne 80 % de la latence edge avec 100 % des capacités Node.

Concrètement, le Worker fait un fetch interne vers https://internal-api.example.sn pointant sur le VPS. Le secret partagé authentifie la requête. Le VPS n’est pas exposé publiquement (firewall Hetzner restreint les IPs entrantes aux ranges Cloudflare). Cette double protection — pas d’exposition publique du VPS, plus authentification par secret rotatif — élimine 99 % des attaques opportunistes que subissent les VPS exposés sur Internet.

Cron Triggers et tâches planifiées

Pour les tâches récurrentes (envoi de digest quotidien, nettoyage de données, synchronisation horaire avec un service tiers), Cloudflare Workers expose les Cron Triggers en plan Paid. Configuration dans wrangler.toml :

[triggers]
crons = ["0 6 * * *", "*/15 * * * *"]

Côté code, on exporte un handler scheduled en plus du fetch. Le runtime appelle ce handler aux moments programmés, sans aucune machine à provisionner. Pour un blog qui pousse les nouveaux articles à 6 h chaque matin, c’est plus simple et moins cher qu’un cron Hetzner.

Sécurité spécifique aux Workers

Trois mesures essentielles. Vérification d’origine sur les endpoints sensibles : la requête doit venir du domaine attendu (header Origin), sinon rejet 403. Cela bloque les CSRF triviaux. Rate limiting natif via le binding Rate Limiting de Cloudflare (en bêta puis GA en 2025) qui distribue le compteur sur tous les PoPs et coûte fraction de centime par million de checks. Protection bot via Cloudflare Turnstile devant les endpoints publics d’inscription ou de contact, qui empêche les bots automatisés sans gêner les utilisateurs légitimes.

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é