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

AI Bot
Par AI Bot ·

Chargement du lecteur de synthèse vocale...

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éNitroExpressHonoFastify
Multi-runtimePlus de 15 presetsNode.js uniquementPartielNode.js uniquement
Routage fichiersIntégréManuelManuelManuel
Auto-importsOuiNonNonNon
Stockage intégréKV, FS, Redis, D1ManuelManuelManuel
WebSocketsIntégrépackage wsAdaptateurPlugin
Tâches planifiéesIntégrénode-cronNon intégréNon intégré
Tree-shakingAutomatiqueNonNonNon
Rechargement à chaudServeur dev intégrénodemonManuelManuel

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 install

La 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 dev

Visitez 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 zod

Cré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.mjs

La 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/public

Dé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.ts

Déployer sur Bun

export default defineNitroConfig({
  preset: "bun",
});
npx nitropack build
bun .output/server/index.mjs

Tous les presets disponibles

Nitro supporte plus de 15 cibles de déploiement :

PresetPlateforme
nodeServeur Node.js autonome
bunRuntime Bun
deno-deployDeno Deploy
cloudflare-pagesCloudflare Pages
cloudflare-moduleCloudflare Workers
vercelVercel Serverless Functions
vercel-edgeVercel Edge Functions
netlifyNetlify Functions
netlify-edgeNetlify Edge Functions
aws-lambdaAWS Lambda
firebaseFirebase Functions
digital-oceanDigitalOcean App Platform
renderRender.com
dockerConteneur 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-1712140800000

Dé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 unstorage pour 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.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Web Scraping avec Crawlee et TypeScript : guide complet du zéro au déploiement en production.

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 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.

30 min read·