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

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 wranglerPuis authentifiez-vous avec votre compte Cloudflare :
wrangler loginCela ouvre une fenêtre de navigateur. Autorisez Wrangler, puis vérifiez la connexion :
wrangler whoamiVous 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-apiLorsqu'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 installVotre 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-dbLa 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.sqlPuis à la base de données distante (production) :
wrangler d1 execute task-db --remote --file=./schema.sqlLe 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 devVotre 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 initCela 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 --remoteLes 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 deploySortie :
⛅️ 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/tasksZé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 deployVotre 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 tailVous 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 --inspectPuis 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ère | Cloudflare Workers + D1 | AWS Lambda + RDS | Vercel Serverless |
|---|---|---|---|
| Démarrage à froid | < 5 ms | 100-500 ms | 50-250 ms |
| Distribution mondiale | 300+ emplacements | Par région | Par région |
| Latence BDD | D1 co-localisé | Dépend du VPC | BDD externe |
| Plan gratuit | 100K req/jour | 1M req/mois | 100K req/mois |
| Tarif (payant) | 0,30 $/million req | 0,20 $/million + calcul | 0,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.
-
Utilisez le batching D1 pour les opérations groupées —
db.batch([stmt1, stmt2])exécute plusieurs instructions en un seul aller-retour. -
Activez Smart Placement dans
wrangler.tomlpour que Cloudflare place automatiquement votre Worker près de votre base D1 :[placement] mode = "smart" -
Utilisez les bindings pour les secrets au lieu de coder les clés API en dur :
wrangler secret put API_KEYAccédez-y via
c.env.API_KEYdans votre code. -
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' })); -
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 ! 🚀
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.

Construire des agents IA from scratch avec TypeScript : maîtriser le pattern ReAct avec le Vercel AI SDK
Apprenez à construire des agents IA depuis zéro avec TypeScript. Ce tutoriel couvre le pattern ReAct, l'appel d'outils, le raisonnement multi-étapes et les boucles d'agents prêtes pour la production avec le Vercel AI SDK.

Construire votre premier serveur MCP avec TypeScript : Outils, Ressources et Prompts
Apprenez a construire un serveur MCP pret pour la production en partant de zero avec TypeScript. Ce tutoriel pratique couvre les outils, les ressources, les prompts, le transport stdio, et la connexion a Claude Desktop et Cursor.