Nitro + H3 : construire des API serveur TypeScript universelles qui se déploient partout

Une seule base de code. Tous les runtimes. Nitro est le moteur serveur open-source TypeScript derrière Nuxt, Analog et Vinxi. Il compile vos routes API en bundles optimisés pour Node.js, Cloudflare Workers, Vercel Edge, Deno, Bun et plus encore — sans aucun changement de configuration. Dans ce tutoriel, vous construirez une API REST complète et la déploierez partout.
Ce que vous allez apprendre
À la fin de ce tutoriel, vous serez capable de :
- Configurer un projet Nitro autonome de zéro avec TypeScript
- Construire des routes API avec des requêtes/réponses typées via H3
- Créer des middlewares pour l'authentification, la journalisation et CORS
- Intégrer une base de données via la couche de stockage intégrée de Nitro
- Implémenter des endpoints WebSocket pour les fonctionnalités temps réel
- Ajouter des tâches planifiées (cron jobs) côté serveur
- Utiliser les plugins serveur et les hooks de cycle de vie
- Déployer la même base de code sur Node.js, Cloudflare Workers, Vercel et Deno
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 20+ installé (
node --version) - Une expérience en TypeScript (types, async/await)
- Des notions de base sur les API REST (méthodes HTTP, codes de statut)
- Un éditeur de code — VS Code ou Cursor recommandé
- Un compte Cloudflare (optionnel, pour le déploiement Workers)
Pourquoi Nitro ?
La plupart des frameworks serveur TypeScript vous enferment dans un seul runtime. Express ne fonctionne que sur Node.js. Hono cible les runtimes edge. Nitro est différent — il compile votre code serveur en bundles optimisés pour n'importe quel runtime JavaScript :
| Fonctionnalité | Nitro | Express | Hono | Fastify |
|---|---|---|---|---|
| Multi-runtime | Plus de 15 presets | Node.js uniquement | Partiel | Node.js uniquement |
| Routage fichiers | Intégré | Manuel | Manuel | Manuel |
| Auto-imports | Oui | Non | Non | Non |
| Stockage intégré | KV, FS, Redis, D1 | Manuel | Manuel | Manuel |
| WebSockets | Intégré | package ws | Adaptateur | Plugin |
| Tâches planifiées | Intégré | node-cron | Non intégré | Non intégré |
| Tree-shaking | Automatique | Non | Non | Non |
| Rechargement à chaud | Serveur dev intégré | nodemon | Manuel | Manuel |
Nitro est propulsé par H3, un framework HTTP minimaliste construit pour la performance. H3 gère le parsing des requêtes, le routage et les réponses — tandis que Nitro ajoute le routage basé sur les fichiers, les auto-imports, l'optimisation du build et les presets de déploiement.
Étape 1 : Créer un nouveau projet Nitro
Initialisez un projet Nitro :
npx giget@latest nitro nitro-api && cd nitro-api
npm installLa structure du projet ressemble à ceci :
nitro-api/
├── routes/
│ └── index.ts # GET /
├── nitro.config.ts # Configuration Nitro
├── tsconfig.json
└── package.json
Lancez le serveur de développement :
npx nitropack devVisitez http://localhost:3000 — vous devriez voir la réponse par défaut. Le serveur de développement intègre le remplacement de modules à chaud, donc les changements apparaissent instantanément.
Étape 2 : Construire les routes API avec H3
Nitro utilise le routage basé sur les fichiers. Chaque fichier dans le répertoire routes/ devient un endpoint API. Le chemin du fichier correspond directement au chemin URL.
Routes de base
Créez routes/api/health.ts :
export default defineEventHandler(() => {
return { status: "ok", timestamp: Date.now() };
});Ceci répond à GET /api/health. Nitro importe automatiquement defineEventHandler — aucune instruction d'import nécessaire.
Paramètres de route
Créez routes/api/users/[id].ts :
export default defineEventHandler((event) => {
const id = getRouterParam(event, "id");
return {
user: {
id,
name: `User ${id}`,
email: `user${id}@example.com`,
},
};
});Accédez-y via GET /api/users/42. Le [id] dans le nom du fichier devient un paramètre dynamique.
Routage par méthode HTTP
Créez des handlers spécifiques à chaque méthode en nommant les fichiers avec le suffixe de méthode :
routes/
└── api/
└── posts/
├── index.get.ts # GET /api/posts
├── index.post.ts # POST /api/posts
└── [id].put.ts # PUT /api/posts/:id
└── [id].delete.ts # DELETE /api/posts/:id
Créez routes/api/posts/index.get.ts :
export default defineEventHandler(() => {
return {
posts: [
{ id: 1, title: "Premiers pas avec Nitro", published: true },
{ id: 2, title: "H3 en profondeur", published: false },
],
};
});Créez routes/api/posts/index.post.ts :
export default defineEventHandler(async (event) => {
const body = await readBody(event);
if (!body.title) {
throw createError({
statusCode: 400,
statusMessage: "Le titre est requis",
});
}
return {
post: {
id: Math.floor(Math.random() * 1000),
title: body.title,
content: body.content || "",
createdAt: new Date().toISOString(),
},
};
});Validation typée des requêtes
Pour une validation robuste des entrées, utilisez readValidatedBody de H3 avec une bibliothèque de validation :
npm install zodCréez routes/api/posts/index.post.ts avec la validation Zod :
import { z } from "zod";
const PostSchema = z.object({
title: z.string().min(3).max(200),
content: z.string().optional().default(""),
tags: z.array(z.string()).optional().default([]),
published: z.boolean().optional().default(false),
});
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, PostSchema.parse);
return {
post: {
id: Math.floor(Math.random() * 1000),
...body,
createdAt: new Date().toISOString(),
},
};
});Si la validation échoue, H3 renvoie automatiquement une erreur 400 avec les détails.
Étape 3 : Middlewares et utilitaires
Middleware de requête
Créez des fichiers dans le répertoire middleware/ qui s'exécutent avant chaque handler de route.
Créez middleware/logger.ts :
export default defineEventHandler((event) => {
const method = getMethod(event);
const path = getRequestURL(event).pathname;
console.log(`[${new Date().toISOString()}] ${method} ${path}`);
});Ceci enregistre chaque requête entrante. Les handlers middleware qui ne retournent pas de valeur passent le contrôle au handler suivant.
Middleware spécifique à une route
Créez middleware/api/auth.ts — il ne s'exécute que pour les routes /api/* :
export default defineEventHandler((event) => {
const authHeader = getHeader(event, "authorization");
if (!authHeader?.startsWith("Bearer ")) {
throw createError({
statusCode: 401,
statusMessage: "En-tête d'autorisation manquant ou invalide",
});
}
const token = authHeader.slice(7);
// En production, vérifiez le JWT ici
event.context.userId = token;
});Accédez à l'ID utilisateur dans n'importe quelle route API via event.context.userId.
Configuration CORS
Nitro supporte CORS de manière intégrée. Mettez à jour nitro.config.ts :
export default defineNitroConfig({
routeRules: {
"/api/**": {
cors: true,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
},
},
},
});Utilitaires partagés
Créez des utilitaires réutilisables dans le répertoire utils/. Ils sont auto-importés dans tout le projet.
Créez utils/response.ts :
export function successResponse<T>(data: T, message = "Succès") {
return {
success: true,
message,
data,
};
}
export function paginatedResponse<T>(
data: T[],
page: number,
limit: number,
total: number
) {
return {
success: true,
data,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
},
};
}Utilisez-les dans n'importe quelle route sans import :
export default defineEventHandler(() => {
return successResponse({ version: "1.0.0" });
});Étape 4 : Intégration base de données avec la couche de stockage
Nitro inclut une couche de stockage universelle propulsée par unstorage. Elle fournit une API clé-valeur qui fonctionne avec plusieurs backends — système de fichiers, Redis, Cloudflare KV, Vercel KV, et plus.
Configurer le stockage
Mettez à jour nitro.config.ts :
export default defineNitroConfig({
storage: {
posts: {
driver: "fs",
base: ".data/posts",
},
cache: {
driver: "memory",
},
},
});Utiliser le stockage dans les routes
Créez routes/api/posts/index.get.ts :
export default defineEventHandler(async () => {
const keys = await useStorage("posts").getKeys();
const posts = await Promise.all(
keys.map(async (key) => {
return await useStorage("posts").getItem(key);
})
);
return successResponse(posts.filter(Boolean));
});Créez routes/api/posts/index.post.ts :
import { z } from "zod";
const PostSchema = z.object({
title: z.string().min(3),
content: z.string(),
tags: z.array(z.string()).default([]),
});
export default defineEventHandler(async (event) => {
const body = await readValidatedBody(event, PostSchema.parse);
const id = `post-${Date.now()}`;
const post = {
id,
...body,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
await useStorage("posts").setItem(id, post);
setResponseStatus(event, 201);
return successResponse(post, "Article créé");
});Créez routes/api/posts/[id].get.ts :
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
const post = await useStorage("posts").getItem(id!);
if (!post) {
throw createError({
statusCode: 404,
statusMessage: "Article non trouvé",
});
}
return successResponse(post);
});Créez routes/api/posts/[id].delete.ts :
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
const exists = await useStorage("posts").hasItem(id!);
if (!exists) {
throw createError({
statusCode: 404,
statusMessage: "Article non trouvé",
});
}
await useStorage("posts").removeItem(id!);
return successResponse(null, "Article supprimé");
});Passer à Redis en production
Pour utiliser Redis au lieu du système de fichiers, changez la configuration de stockage :
export default defineNitroConfig({
storage: {
posts: {
driver: "redis",
url: process.env.REDIS_URL || "redis://localhost:6379",
},
},
});Aucun changement de code dans les routes — l'API de stockage reste identique quel que soit le backend.
Étape 5 : Mise en cache avec les règles de routes
Nitro dispose d'une mise en cache intégrée. Cachez les réponses API sans modifier le code grâce aux règles de routes :
export default defineNitroConfig({
routeRules: {
"/api/posts": {
cache: {
maxAge: 60, // 60 secondes
},
},
"/api/health": {
cache: {
maxAge: 10,
},
},
"/api/posts/**": {
cache: false, // Pas de cache pour les articles individuels
},
},
});Pour une mise en cache programmatique, utilisez cachedEventHandler :
export default cachedEventHandler(
async () => {
// Cette opération coûteuse est mise en cache
const allPosts = await fetchAllPosts();
const stats = computeStats(allPosts);
return successResponse(stats);
},
{
maxAge: 300, // 5 minutes
name: "post-stats",
}
);Étape 6 : Support WebSocket
Nitro supporte les WebSockets via l'utilitaire defineWebSocketHandler.
Créez routes/ws.ts :
export default defineWebSocketHandler({
open(peer) {
console.log(`[ws] Client connecté : ${peer.id}`);
peer.send(JSON.stringify({ type: "welcome", id: peer.id }));
peer.subscribe("chat");
},
message(peer, message) {
const data = JSON.parse(message.text());
console.log(`[ws] Message de ${peer.id} :`, data);
// Diffuser à tous les abonnés
peer.publish(
"chat",
JSON.stringify({
type: "message",
from: peer.id,
text: data.text,
timestamp: Date.now(),
})
);
},
close(peer) {
console.log(`[ws] Client déconnecté : ${peer.id}`);
},
});Activez le support WebSocket dans nitro.config.ts :
export default defineNitroConfig({
experimental: {
websocket: true,
},
});Connexion depuis un client :
const ws = new WebSocket("ws://localhost:3000/ws");
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("Reçu :", data);
};
ws.onopen = () => {
ws.send(JSON.stringify({ text: "Bonjour depuis le client !" }));
};Étape 7 : Tâches planifiées
Nitro supporte les tâches planifiées de type cron qui s'exécutent côté serveur.
Créez tasks/cleanup.ts :
export default defineTask({
meta: {
name: "cleanup",
description: "Supprimer les données expirées du stockage",
},
async run() {
const keys = await useStorage("posts").getKeys();
let removed = 0;
for (const key of keys) {
const post: any = await useStorage("posts").getItem(key);
if (!post) continue;
const age = Date.now() - new Date(post.createdAt).getTime();
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
if (age > thirtyDays && !post.published) {
await useStorage("posts").removeItem(key);
removed++;
}
}
return { result: `${removed} brouillons expirés supprimés` };
},
});Configurez la planification dans nitro.config.ts :
export default defineNitroConfig({
experimental: {
tasks: true,
},
scheduledTasks: {
// Exécuter le nettoyage chaque jour à minuit
"0 0 * * *": ["cleanup"],
},
});En développement, déclenchez les tâches manuellement via l'endpoint intégré :
curl http://localhost:3000/_nitro/tasks/cleanupÉtape 8 : Plugins serveur et cycle de vie
Créez des plugins qui s'exécutent au démarrage du serveur. Utilisez-les pour les connexions aux bases de données, l'initialisation des services ou la journalisation.
Créez plugins/startup.ts :
export default defineNitroPlugin((nitroApp) => {
console.log("[plugin] Démarrage du serveur...");
// Hook dans le cycle de vie des requêtes
nitroApp.hooks.hook("request", (event) => {
event.context.requestStartTime = Date.now();
});
nitroApp.hooks.hook("afterResponse", (event) => {
const duration = Date.now() - (event.context.requestStartTime || 0);
const path = getRequestURL(event).pathname;
console.log(`[perf] ${path} - ${duration}ms`);
});
// Hook sur la fermeture du serveur
nitroApp.hooks.hook("close", () => {
console.log("[plugin] Arrêt du serveur...");
});
});Étape 9 : Gestion des erreurs
Créez un gestionnaire d'erreurs global pour standardiser les réponses d'erreur.
Créez plugins/error-handler.ts :
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook("error", (error, { event }) => {
console.error(`[error] ${error.message}`, {
path: event ? getRequestURL(event).pathname : "unknown",
stack: error.stack,
});
});
});Créez des réponses d'erreur personnalisées dans les routes :
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
if (!id || !/^\d+$/.test(id)) {
throw createError({
statusCode: 400,
statusMessage: "Format d'ID invalide",
data: {
field: "id",
expected: "chaîne numérique",
received: id,
},
});
}
// Continuer le traitement...
});Étape 10 : Déployer partout
C'est ici que Nitro brille vraiment. La même base de code se déploie sur n'importe quelle plateforme en changeant une seule valeur de configuration.
Déployer sur Node.js (par défaut)
npx nitropack build
node .output/server/index.mjsLa sortie du build est un serveur autonome dans .output/ — pas besoin de node_modules en production.
Déployer sur Cloudflare Workers
Mettez à jour nitro.config.ts :
export default defineNitroConfig({
preset: "cloudflare-pages",
});Build et déploiement :
npx nitropack build
npx wrangler pages deploy .output/publicDéployer sur Vercel
export default defineNitroConfig({
preset: "vercel-edge",
});Poussez vers Git — Vercel détecte et déploie automatiquement.
Déployer sur Deno Deploy
export default defineNitroConfig({
preset: "deno-deploy",
});npx nitropack build
deployctl deploy --project=my-api .output/server/index.tsDéployer sur Bun
export default defineNitroConfig({
preset: "bun",
});npx nitropack build
bun .output/server/index.mjsTous les presets disponibles
Nitro supporte plus de 15 cibles de déploiement :
| Preset | Plateforme |
|---|---|
node | Serveur Node.js autonome |
bun | Runtime Bun |
deno-deploy | Deno Deploy |
cloudflare-pages | Cloudflare Pages |
cloudflare-module | Cloudflare Workers |
vercel | Vercel Serverless Functions |
vercel-edge | Vercel Edge Functions |
netlify | Netlify Functions |
netlify-edge | Netlify Edge Functions |
aws-lambda | AWS Lambda |
firebase | Firebase Functions |
digital-ocean | DigitalOcean App Platform |
render | Render.com |
docker | Conteneur Docker |
Étape 11 : Configuration de production
Créez une configuration prête pour la production :
export default defineNitroConfig({
// Cible de déploiement
preset: process.env.NITRO_PRESET || "node",
// Cache au niveau des routes
routeRules: {
"/api/**": {
cors: true,
headers: {
"Access-Control-Allow-Origin": "*",
},
},
"/api/posts": {
cache: { maxAge: 60 },
},
},
// Backends de stockage
storage: {
posts: {
driver: process.env.REDIS_URL ? "redis" : "fs",
...(process.env.REDIS_URL
? { url: process.env.REDIS_URL }
: { base: ".data/posts" }),
},
},
// Configuration runtime (accessible via useRuntimeConfig())
runtimeConfig: {
apiSecret: process.env.API_SECRET || "dev-secret",
public: {
apiBase: process.env.API_BASE || "http://localhost:3000",
},
},
// Activer les fonctionnalités expérimentales
experimental: {
websocket: true,
tasks: true,
},
// Tâches planifiées
scheduledTasks: {
"0 0 * * *": ["cleanup"],
},
// Compression
compressPublicAssets: true,
});Accéder à la configuration runtime dans les routes :
export default defineEventHandler(() => {
const config = useRuntimeConfig();
return {
apiBase: config.public.apiBase,
// Ne jamais exposer les secrets dans les réponses
};
});Tester votre API
Testez l'API complète avec curl :
# Vérification de santé
curl http://localhost:3000/api/health
# Créer un article
curl -X POST http://localhost:3000/api/posts \
-H "Content-Type: application/json" \
-d '{"title": "Mon premier article", "content": "Bonjour Nitro !", "tags": ["intro"]}'
# Lister tous les articles
curl http://localhost:3000/api/posts
# Obtenir un article spécifique (utilisez l'ID de la réponse de création)
curl http://localhost:3000/api/posts/post-1712140800000
# Supprimer un article
curl -X DELETE http://localhost:3000/api/posts/post-1712140800000Dépannage
Erreurs "Cannot find module"
Nitro importe automatiquement les utilitaires de H3 et de son propre runtime. Si votre éditeur affiche des erreurs, assurez-vous que tsconfig.json étend les types générés :
{
"extends": "./.nitro/types/tsconfig.json"
}Exécutez npx nitropack prepare pour générer les types.
Le stockage ne persiste pas
En développement, le driver système de fichiers stocke les données dans .data/. Assurez-vous que ce répertoire existe et est accessible en écriture. En production, utilisez Redis ou un driver de stockage cloud natif.
Connexion WebSocket refusée
Assurez-vous que experimental.websocket est défini sur true dans nitro.config.ts. Certaines plateformes de déploiement (comme Cloudflare Pages) nécessitent une configuration spécifique pour le support WebSocket.
Sortie de build trop volumineuse
Nitro effectue le tree-shaking automatiquement. Si la sortie reste volumineuse, vérifiez les dépendances lourdes importées au niveau supérieur. Utilisez les imports dynamiques pour les fonctionnalités optionnelles :
export default defineEventHandler(async () => {
const { heavyLibrary } = await import("heavy-library");
return heavyLibrary.process();
});Prochaines étapes
- Ajouter une base de données : connectez Drizzle ORM ou Prisma pour le support des bases SQL
- Ajouter l'authentification : implémentez la vérification JWT dans un middleware
- Ajouter le rate limiting : utilisez la couche de cache de Nitro avec
unstoragepour le rate limiting - Construire un frontend : associez avec n'importe quel framework frontend — React, Vue, Svelte, ou HTML pur
- Explorer Nuxt : si vous avez besoin d'un framework full-stack, Nuxt utilise Nitro comme moteur serveur avec les mêmes API
Conclusion
Nitro et H3 vous donnent un avantage unique dans l'écosystème serveur TypeScript : écrire une fois, déployer partout. Vous avez construit une API REST complète avec une validation typée, des middlewares, de la mise en cache, des WebSockets, des tâches planifiées et une couche de stockage universelle. La même base de code fonctionne sur Node.js, Cloudflare Workers, Vercel Edge, Deno et Bun sans changer une seule ligne de code applicatif.
Les concepts clés que vous avez appris :
- Le routage basé sur les fichiers mappe les chemins du système de fichiers aux endpoints API
- Les utilitaires H3 fournissent une gestion typée des requêtes/réponses
- La couche de stockage abstrait le backend de base de données
- Les règles de routes configurent le cache et les en-têtes de manière déclarative
- Les presets de déploiement compilent votre code pour n'importe quel runtime
Nitro a fait ses preuves en production — il sert des millions de requêtes quotidiennement en tant que moteur derrière les applications Nuxt dans le monde entier. Que vous construisiez un microservice, une API REST, ou le backend d'une application full-stack, Nitro garantit que votre code serveur est portable, performant et prêt pour le futur.
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

Créer et déployer une API serverless avec Cloudflare Workers, Hono et D1
Apprenez à construire une API REST prête pour la production avec Cloudflare Workers, le framework Hono et la base de données D1 — de la configuration initiale au déploiement mondial.

Construire une API GraphQL typesafe avec Next.js App Router, Yoga et Pothos
Apprenez à construire une API GraphQL entièrement typesafe avec Next.js 15 App Router, GraphQL Yoga et le constructeur de schémas Pothos. Ce tutoriel pratique couvre la conception de schémas, les requêtes, les mutations, le middleware d'authentification et un client React avec urql.

Mistral AI API avec TypeScript : Créer des Applications Intelligentes
Apprenez à utiliser l'API Mistral AI avec TypeScript pour créer des applications intelligentes : chat, génération de texte structuré, appels de fonctions et RAG.