Créer et déployer une API serverless avec Cloudflare Workers, Hono et D1

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

Déployez vos API en périphérie, partout dans le monde, en quelques secondes. Cloudflare Workers exécute votre code dans plus de 300 data centers sans aucun démarrage à froid. Combiné au routage ultra-rapide de Hono et à la base de données SQLite serverless D1, vous obtenez une API complète sans gérer le moindre serveur.

Ce que vous allez construire

Dans ce tutoriel, vous allez créer une API de gestion de tâches complète avec des opérations CRUD, adossée à une base de données D1 SQLite, construite avec le framework Hono, et déployée mondialement sur Cloudflare Workers. À la fin, vous disposerez d'une API de production fonctionnant en périphérie du réseau.

Fonctionnalités de l'API finale :

  • Points d'accès RESTful pour les tâches (créer, lire, modifier, supprimer)
  • Validation des entrées et gestion des erreurs
  • Base de données SQLite avec système de migrations
  • Support CORS pour la consommation côté frontend
  • Déploiement mondial avec une latence quasi nulle

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Node.js 18+ installé (télécharger ici)
  • Un compte Cloudflare — le plan gratuit suffit (inscription)
  • Wrangler CLI — l'outil de développement Cloudflare (nous l'installerons ensemble)
  • Des connaissances de base en TypeScript et en API REST
  • Un éditeur de code (VS Code recommandé)

Le plan gratuit de Cloudflare Workers inclut 100 000 requêtes/jour et D1 offre 5 millions de lectures de lignes/jour — largement suffisant pour la plupart des projets et prototypes.


Étape 1 : Installer Wrangler et s'authentifier

Wrangler est l'outil en ligne de commande pour développer et déployer des Cloudflare Workers. Installez-le globalement :

npm install -g wrangler

Puis authentifiez-vous avec votre compte Cloudflare :

wrangler login

Cela ouvre une fenêtre de navigateur. Autorisez Wrangler, puis vérifiez la connexion :

wrangler whoami

Vous devriez voir le nom et l'identifiant de votre compte.


Étape 2 : Initialiser le projet

Créez un nouveau projet Hono configuré pour Cloudflare Workers :

npm create hono@latest task-api

Lorsqu'on vous demande :

  • Quel template ?cloudflare-workers
  • Gestionnaire de paquets ?npm (ou votre préférence)

Naviguez dans le dossier du projet :

cd task-api
npm install

Votre structure de projet ressemble à ceci :

task-api/
├── src/
│   └── index.ts        # Point d'entrée principal
├── wrangler.toml       # Configuration Cloudflare
├── package.json
└── tsconfig.json

Étape 3 : Créer la base de données D1

D1 est la base de données SQLite serverless de Cloudflare. Créez-en une pour votre projet :

wrangler d1 create task-db

La sortie inclura un identifiant de base de données. Copiez-le — vous en aurez besoin pour la configuration :

✅ Successfully created DB 'task-db'

[[d1_databases]]
binding = "DB"
database_name = "task-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

Ouvrez wrangler.toml et ajoutez le binding D1 :

name = "task-api"
main = "src/index.ts"
compatibility_date = "2026-02-25"
 
[[d1_databases]]
binding = "DB"
database_name = "task-db"
database_id = "VOTRE_ID_DE_BASE_DE_DONNEES"

Remplacez VOTRE_ID_DE_BASE_DE_DONNEES par l'identifiant réel obtenu précédemment.


Étape 4 : Définir le schéma de la base de données

Créez un fichier schema.sql à la racine de votre projet :

-- schema.sql
DROP TABLE IF EXISTS tasks;
 
CREATE TABLE tasks (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  description TEXT DEFAULT '',
  status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed')),
  priority INTEGER DEFAULT 0 CHECK(priority BETWEEN 0 AND 3),
  created_at TEXT DEFAULT (datetime('now')),
  updated_at TEXT DEFAULT (datetime('now'))
);
 
-- Données initiales pour les tests
INSERT INTO tasks (title, description, status, priority) VALUES
  ('Configurer le pipeline CI/CD', 'Mettre en place GitHub Actions pour les déploiements automatisés', 'pending', 2),
  ('Rédiger la documentation API', 'Créer la spécification OpenAPI pour l''API de gestion de tâches', 'in_progress', 1),
  ('Concevoir le schéma de base de données', 'Finaliser le diagramme ERD du projet', 'completed', 3);

Appliquez le schéma à votre base de données locale (développement) :

wrangler d1 execute task-db --local --file=./schema.sql

Puis à la base de données distante (production) :

wrangler d1 execute task-db --remote --file=./schema.sql

Le flag --remote modifie directement votre base de données de production. Dans un vrai projet, utilisez les migrations D1 (wrangler d1 migrations) pour des changements de schéma sûrs et versionnés.


Étape 5 : Définir les types TypeScript

Créez src/types.ts pour définir vos modèles de données et bindings :

// src/types.ts
 
export interface Env {
  DB: D1Database;
}
 
export interface Task {
  id: number;
  title: string;
  description: string;
  status: 'pending' | 'in_progress' | 'completed';
  priority: number;
  created_at: string;
  updated_at: string;
}
 
export interface CreateTaskInput {
  title: string;
  description?: string;
  status?: Task['status'];
  priority?: number;
}
 
export interface UpdateTaskInput {
  title?: string;
  description?: string;
  status?: Task['status'];
  priority?: number;
}

Étape 6 : Construire les routes de l'API

Passons au cœur de l'application. Remplacez le contenu de src/index.ts par l'application Hono complète :

// src/index.ts
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import type { Env, Task, CreateTaskInput, UpdateTaskInput } from './types';
 
const app = new Hono<{ Bindings: Env }>();
 
// ─── Middleware ───────────────────────────────────────────────
app.use('/*', cors());
 
// ─── Vérification de santé ───────────────────────────────────
app.get('/', (c) => {
  return c.json({
    status: 'ok',
    service: 'Task Management API',
    version: '1.0.0',
    timestamp: new Date().toISOString(),
  });
});
 
// ─── GET /tasks ──────────────────────────────────────────────
// Lister toutes les tâches avec filtrage optionnel
app.get('/tasks', async (c) => {
  const status = c.req.query('status');
  const sortBy = c.req.query('sort') || 'created_at';
  const order = c.req.query('order') || 'desc';
  const limit = Math.min(parseInt(c.req.query('limit') || '50'), 100);
  const offset = parseInt(c.req.query('offset') || '0');
 
  let query = 'SELECT * FROM tasks';
  const params: string[] = [];
 
  if (status) {
    query += ' WHERE status = ?';
    params.push(status);
  }
 
  const allowedSorts = ['created_at', 'updated_at', 'priority', 'title'];
  const safeSort = allowedSorts.includes(sortBy) ? sortBy : 'created_at';
  const safeOrder = order.toUpperCase() === 'ASC' ? 'ASC' : 'DESC';
 
  query += ` ORDER BY ${safeSort} ${safeOrder} LIMIT ? OFFSET ?`;
  params.push(limit.toString(), offset.toString());
 
  try {
    const result = await c.env.DB.prepare(query)
      .bind(...params)
      .all<Task>();
 
    return c.json({
      tasks: result.results,
      meta: {
        total: result.results.length,
        limit,
        offset,
      },
    });
  } catch (error) {
    return c.json({ error: 'Échec de la récupération des tâches' }, 500);
  }
});
 
// ─── GET /tasks/:id ─────────────────────────────────────────
// Récupérer une tâche par son identifiant
app.get('/tasks/:id', async (c) => {
  const id = c.req.param('id');
 
  try {
    const task = await c.env.DB.prepare(
      'SELECT * FROM tasks WHERE id = ?'
    )
      .bind(id)
      .first<Task>();
 
    if (!task) {
      return c.json({ error: 'Tâche introuvable' }, 404);
    }
 
    return c.json({ task });
  } catch (error) {
    return c.json({ error: 'Échec de la récupération de la tâche' }, 500);
  }
});
 
// ─── POST /tasks ─────────────────────────────────────────────
// Créer une nouvelle tâche
app.post('/tasks', async (c) => {
  let body: CreateTaskInput;
 
  try {
    body = await c.req.json<CreateTaskInput>();
  } catch {
    return c.json({ error: 'Corps JSON invalide' }, 400);
  }
 
  if (!body.title || body.title.trim().length === 0) {
    return c.json({ error: 'Le titre est obligatoire' }, 400);
  }
 
  if (body.title.length > 255) {
    return c.json({ error: 'Le titre ne doit pas dépasser 255 caractères' }, 400);
  }
 
  const validStatuses = ['pending', 'in_progress', 'completed'];
  if (body.status && !validStatuses.includes(body.status)) {
    return c.json({ error: `Le statut doit être l'un de : ${validStatuses.join(', ')}` }, 400);
  }
 
  if (body.priority !== undefined && (body.priority < 0 || body.priority > 3)) {
    return c.json({ error: 'La priorité doit être comprise entre 0 et 3' }, 400);
  }
 
  try {
    const result = await c.env.DB.prepare(
      `INSERT INTO tasks (title, description, status, priority)
       VALUES (?, ?, ?, ?)
       RETURNING *`
    )
      .bind(
        body.title.trim(),
        body.description || '',
        body.status || 'pending',
        body.priority ?? 0
      )
      .first<Task>();
 
    return c.json({ task: result }, 201);
  } catch (error) {
    return c.json({ error: 'Échec de la création de la tâche' }, 500);
  }
});
 
// ─── PUT /tasks/:id ─────────────────────────────────────────
// Mettre à jour une tâche existante
app.put('/tasks/:id', async (c) => {
  const id = c.req.param('id');
  let body: UpdateTaskInput;
 
  try {
    body = await c.req.json<UpdateTaskInput>();
  } catch {
    return c.json({ error: 'Corps JSON invalide' }, 400);
  }
 
  const existing = await c.env.DB.prepare(
    'SELECT * FROM tasks WHERE id = ?'
  )
    .bind(id)
    .first<Task>();
 
  if (!existing) {
    return c.json({ error: 'Tâche introuvable' }, 404);
  }
 
  const updates: string[] = [];
  const values: (string | number)[] = [];
 
  if (body.title !== undefined) {
    if (body.title.trim().length === 0) {
      return c.json({ error: 'Le titre ne peut pas être vide' }, 400);
    }
    updates.push('title = ?');
    values.push(body.title.trim());
  }
 
  if (body.description !== undefined) {
    updates.push('description = ?');
    values.push(body.description);
  }
 
  if (body.status !== undefined) {
    const validStatuses = ['pending', 'in_progress', 'completed'];
    if (!validStatuses.includes(body.status)) {
      return c.json({ error: `Le statut doit être l'un de : ${validStatuses.join(', ')}` }, 400);
    }
    updates.push('status = ?');
    values.push(body.status);
  }
 
  if (body.priority !== undefined) {
    if (body.priority < 0 || body.priority > 3) {
      return c.json({ error: 'La priorité doit être comprise entre 0 et 3' }, 400);
    }
    updates.push('priority = ?');
    values.push(body.priority);
  }
 
  if (updates.length === 0) {
    return c.json({ error: 'Aucun champ à mettre à jour' }, 400);
  }
 
  updates.push("updated_at = datetime('now')");
  values.push(id);
 
  try {
    const result = await c.env.DB.prepare(
      `UPDATE tasks SET ${updates.join(', ')} WHERE id = ? RETURNING *`
    )
      .bind(...values)
      .first<Task>();
 
    return c.json({ task: result });
  } catch (error) {
    return c.json({ error: 'Échec de la mise à jour de la tâche' }, 500);
  }
});
 
// ─── DELETE /tasks/:id ───────────────────────────────────────
// Supprimer une tâche
app.delete('/tasks/:id', async (c) => {
  const id = c.req.param('id');
 
  try {
    const existing = await c.env.DB.prepare(
      'SELECT id FROM tasks WHERE id = ?'
    )
      .bind(id)
      .first();
 
    if (!existing) {
      return c.json({ error: 'Tâche introuvable' }, 404);
    }
 
    await c.env.DB.prepare('DELETE FROM tasks WHERE id = ?')
      .bind(id)
      .run();
 
    return c.json({ message: 'Tâche supprimée avec succès' });
  } catch (error) {
    return c.json({ error: 'Échec de la suppression de la tâche' }, 500);
  }
});
 
// ─── Gestionnaire 404 ───────────────────────────────────────
app.notFound((c) => {
  return c.json({ error: 'Non trouvé' }, 404);
});
 
// ─── Gestionnaire d'erreurs ─────────────────────────────────
app.onError((err, c) => {
  console.error('Erreur inattendue :', err);
  return c.json({ error: 'Erreur interne du serveur' }, 500);
});
 
export default app;

Étape 7 : Tester en local

Wrangler fournit un serveur de développement local qui émule l'environnement Workers, y compris D1 :

wrangler dev

Votre API tourne maintenant sur http://localhost:8787. Testez-la avec curl :

# Vérification de santé
curl http://localhost:8787/
 
# Lister toutes les tâches
curl http://localhost:8787/tasks
 
# Créer une nouvelle tâche
curl -X POST http://localhost:8787/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Apprendre Cloudflare Workers", "description": "Terminer le tutoriel", "priority": 2}'
 
# Récupérer une tâche spécifique
curl http://localhost:8787/tasks/1
 
# Mettre à jour une tâche
curl -X PUT http://localhost:8787/tasks/1 \
  -H "Content-Type: application/json" \
  -d '{"status": "completed"}'
 
# Supprimer une tâche
curl -X DELETE http://localhost:8787/tasks/4
 
# Filtrer par statut
curl "http://localhost:8787/tasks?status=pending&sort=priority&order=desc"

Vous obtiendrez des réponses JSON pour chaque opération. La base de données locale persiste entre les redémarrages dans .wrangler/state/.

Rechargement automatique : Wrangler surveille vos fichiers et recharge automatiquement à chaque sauvegarde. Pas besoin de redémarrer le serveur de développement.


Étape 8 : Ajouter un middleware de validation

Pour une API plus robuste, ajoutons un middleware de validation réutilisable. Créez src/middleware.ts :

// src/middleware.ts
import { Context, Next } from 'hono';
 
export function validateJson() {
  return async (c: Context, next: Next) => {
    if (['POST', 'PUT', 'PATCH'].includes(c.req.method)) {
      const contentType = c.req.header('content-type');
      if (!contentType?.includes('application/json')) {
        return c.json(
          { error: 'Le Content-Type doit être application/json' },
          415
        );
      }
    }
    await next();
  };
}
 
export function requestLogger() {
  return async (c: Context, next: Next) => {
    const start = Date.now();
    await next();
    const duration = Date.now() - start;
    console.log(
      `${c.req.method} ${c.req.path} → ${c.res.status} (${duration}ms)`
    );
  };
}
 
export function rateLimit(maxRequests: number, windowMs: number) {
  const requests = new Map<string, { count: number; resetAt: number }>();
 
  return async (c: Context, next: Next) => {
    const ip = c.req.header('cf-connecting-ip') || 'unknown';
    const now = Date.now();
    const record = requests.get(ip);
 
    if (!record || now > record.resetAt) {
      requests.set(ip, { count: 1, resetAt: now + windowMs });
    } else if (record.count >= maxRequests) {
      return c.json({ error: 'Trop de requêtes' }, 429);
    } else {
      record.count++;
    }
 
    await next();
  };
}

Puis ajoutez les middleware à votre index.ts :

import { validateJson, requestLogger } from './middleware';
 
// Ajouter après cors()
app.use('/*', requestLogger());
app.use('/tasks/*', validateJson());

Étape 9 : Mettre en place les migrations D1 (bonne pratique)

Au lieu d'exécuter du SQL brut, utilisez le système de migrations de Wrangler pour la production :

# Créer le répertoire de migrations
wrangler d1 migrations create task-db init

Cela crée un fichier dans migrations/. Collez-y votre SQL de schéma. Puis appliquez :

# Appliquer localement
wrangler d1 migrations apply task-db --local
 
# Appliquer en production
wrangler d1 migrations apply task-db --remote

Les futurs changements de schéma deviennent de nouveaux fichiers de migration, vous donnant un contrôle de version complet sur votre schéma de base de données.


Étape 10 : Déployer en production

Le déploiement se fait en une seule commande :

wrangler deploy

Sortie :

⛅️ wrangler 3.x.x
Uploaded task-api (1.42 sec)
Published task-api (0.35 sec)
  https://task-api.VOTRE_SOUS_DOMAINE.workers.dev

Votre API est désormais en ligne sur le réseau mondial de Cloudflare ! Testez-la :

curl https://task-api.VOTRE_SOUS_DOMAINE.workers.dev/tasks

Zéro démarrage à froid. Contrairement à AWS Lambda ou Google Cloud Functions, les Cloudflare Workers démarrent en moins de 5 ms. Votre API répond instantanément, depuis n'importe quel point du globe.


Étape 11 : Ajouter un domaine personnalisé (optionnel)

Si vous souhaitez votre API sur un domaine personnalisé, ajoutez une route dans wrangler.toml :

routes = [
  { pattern = "api.votredomaine.com/*", zone_name = "votredomaine.com" }
]

Assurez-vous que le domaine est ajouté à votre compte Cloudflare. Puis redéployez :

wrangler deploy

Votre API est maintenant accessible sur https://api.votredomaine.com/tasks.


Étape 12 : Surveiller et déboguer

Cloudflare fournit des logs en temps réel pour vos Workers :

# Suivre les logs en direct depuis la production
wrangler tail

Vous verrez chaque requête, le code de réponse et toute sortie console.log en temps réel. Pour une surveillance plus avancée, consultez le tableau de bord Workers Analytics dans l'interface Cloudflare.

Pour déboguer localement avec des points d'arrêt :

wrangler dev --inspect

Puis connectez Chrome DevTools à l'URL affichée.


Structure finale du projet

task-api/
├── src/
│   ├── index.ts          # Application Hono principale avec routes
│   ├── types.ts          # Interfaces TypeScript
│   └── middleware.ts      # Middleware personnalisés
├── migrations/
│   └── 0001_init.sql     # Migration D1
├── schema.sql            # Schéma initial (référence)
├── wrangler.toml         # Configuration Cloudflare
├── package.json
└── tsconfig.json

Comparaison des performances

Pourquoi choisir cette stack ? Voici la comparaison :

CritèreCloudflare Workers + D1AWS Lambda + RDSVercel Serverless
Démarrage à froid< 5 ms100-500 ms50-250 ms
Distribution mondiale300+ emplacementsPar régionPar région
Latence BDDD1 co-localiséDépend du VPCBDD externe
Plan gratuit100K req/jour1M req/mois100K req/mois
Tarif (payant)0,30 $/million req0,20 $/million + calcul0,60 $/million req

Conseils et bonnes pratiques

Gardez vos Workers légers. La limite de taille compressée de 1 Mo encourage des services petits et ciblés. Si votre API grossit, découpez-la en plusieurs Workers.

  1. Utilisez le batching D1 pour les opérations groupées — db.batch([stmt1, stmt2]) exécute plusieurs instructions en un seul aller-retour.

  2. Activez Smart Placement dans wrangler.toml pour que Cloudflare place automatiquement votre Worker près de votre base D1 :

    [placement]
    mode = "smart"
  3. Utilisez les bindings pour les secrets au lieu de coder les clés API en dur :

    wrangler secret put API_KEY

    Accédez-y via c.env.API_KEY dans votre code.

  4. Exploitez les middleware intégrés de Hono — il propose l'authentification JWT, Bearer, Basic, ETag, et bien plus :

    import { bearerAuth } from 'hono/bearer-auth';
    app.use('/admin/*', bearerAuth({ token: 'secret' }));
  5. Utilisez ctx.executionCtx.waitUntil() pour les tâches non bloquantes comme la journalisation ou l'analytique qui ne doivent pas retarder la réponse.


Et après ?

Vous disposez désormais d'une API serverless de production fonctionnant à l'échelle mondiale. Voici quelques pistes pour aller plus loin :

  • Ajouter l'authentification avec Cloudflare Access ou des tokens JWT
  • Implémenter la pagination par curseur pour les grands ensembles de données
  • Ajouter la recherche plein texte via l'extension FTS5 de D1
  • Créer un frontend en React ou Next.js consommant votre API
  • Mettre en place un CI/CD avec GitHub Actions et wrangler deploy
  • Ajouter du cache avec l'API Cache ou Cloudflare KV pour les endpoints à forte lecture

Résumé

Dans ce tutoriel, vous avez construit une API REST serverless complète en utilisant trois technologies puissantes :

  • Cloudflare Workers — du calcul serverless en périphérie de réseau sans démarrage à froid
  • Hono — un framework web ultra-rapide conçu pour les environnements edge
  • D1 — la base de données SQLite serverless de Cloudflare avec réplication mondiale

Vous avez configuré le projet, défini un schéma de base de données, construit des routes CRUD avec validation et gestion d'erreurs, testé localement et déployé mondialement — le tout sans provisionner ni gérer aucun serveur. L'ensemble de la stack fonctionne sur le plan gratuit de Cloudflare, ce qui en fait un excellent choix pour les projets personnels, les MVP et les API de production.

L'edge computing n'est plus le futur — c'est le présent. À vous de jouer ! 🚀


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Construire un chatbot RAG avec Supabase pgvector et Next.js.

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

Construire un Agent IA Autonome avec Agentic RAG et Next.js

Apprenez a construire un agent IA qui decide de maniere autonome quand et comment recuperer des informations depuis des bases de donnees vectorielles. Un guide pratique complet avec Vercel AI SDK et Next.js, accompagne d'exemples executables.

30 min read·