Sous-solA API dans NuxtPièce 5/7Intermédiaire

Écrire une API complète

Construire les cinq routes d'une API de recettes (lire, créer, modifier, supprimer) avec validation des entrées et erreurs claires.

Ce que tu vas apprendre

  • Organiser les routes d'un CRUD (créer, lire, modifier, supprimer).
  • Valider les données reçues avant de les utiliser.
  • Renvoyer le bon code HTTP dans chaque cas.

Le plan des routes

Le cours Les routes API a montré une route. Une API complète en a cinq pour les recettes, une par fichier :

FichierRequêteRéponse
server/api/recettes/index.get.tsGET /api/recettes200, la liste
server/api/recettes/index.post.tsPOST /api/recettes201, la recette créée
server/api/recettes/[id].get.tsGET /api/recettes/2200 ou 404
server/api/recettes/[id].patch.tsPATCH /api/recettes/2200 ou 404
server/api/recettes/[id].delete.tsDELETE /api/recettes/2204 ou 404

Un stockage partagé

Pour l'exemple, les recettes vivent dans un tableau en mémoire. Un fichier de server/utils/ est importé automatiquement dans les routes :

server/utils/recettes.ts
export type Recette = { id: number, titre: string, duree: number }

export const recettes: Recette[] = [
  { id: 1, titre: 'Crêpes', duree: 20 },
  { id: 2, titre: 'Ratatouille', duree: 60 }
]

export function trouverRecette(id: number) {
  const recette = recettes.find(r => r.id === id)
  if (!recette) {
    throw createError({ status: 404, statusText: 'Recette introuvable' })
  }
  return recette
}
Un tableau en mémoire est vidé à chaque redémarrage du serveur. C'est suffisant pour apprendre ; en vrai, tu utilises une base de données.

Valider les entrées

Tout ce que le client envoie est à vérifier. Une bibliothèque de validation comme Zod (à installer avec npm install zod) décrit la forme attendue une fois pour toutes :

server/utils/schemas.ts
import { z } from 'zod'

export const schemaRecette = z.object({
  titre: z.string().trim().min(1).max(100),
  duree: z.number().int().positive().max(1440)
})

export const schemaModification = schemaRecette.partial()

partial() rend chaque champ facultatif : c'est ce qu'il faut pour un PATCH, qui modifie en partie.

Ensuite, une petite fonction applique le schéma et renvoie un 400 lisible :

server/utils/schemas.ts
export function verifier<T>(schema: z.ZodType<T>, donnees: unknown): T {
  const resultat = schema.safeParse(donnees)
  if (!resultat.success) {
    throw createError({
      status: 400,
      statusText: 'Donnees invalides',
      data: resultat.error.issues
    })
  }
  return resultat.data
}
h3 propose aussi readValidatedBody, qui lit et valide le corps en une étape avec un validateur comme Zod. Écrire verifier toi-même te laisse choisir le code et le message d'erreur.

Créer : POST

server/api/recettes/index.post.ts
export default defineEventHandler(async (event) => {
  const corps = verifier(schemaRecette, await readBody(event))
  const nouvelle = { id: Date.now(), ...corps }
  recettes.push(nouvelle)

  setResponseStatus(event, 201)
  return nouvelle
})

Seuls titre et duree sont repris : si le client envoie id: 1 ou un champ inventé, il est ignoré. setResponseStatus(event, 201) annonce la création.

Lire, modifier, supprimer

Les trois routes d'une recette précise partagent le même début : lire l'identifiant dans l'adresse et le valider.

server/api/recettes/[id].patch.ts
export default defineEventHandler(async (event) => {
  const id = Number(getRouterParam(event, 'id'))
  if (!Number.isInteger(id)) {
    throw createError({ status: 400, statusText: 'Identifiant invalide' })
  }

  const recette = trouverRecette(id)
  const modifs = verifier(schemaModification, await readBody(event))
  return Object.assign(recette, modifs)
})

[id].get.ts fait pareil sans lire le corps, et renvoie trouverRecette(id). Pour la suppression :

server/api/recettes/[id].delete.ts
export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, 'id'))
  const recette = trouverRecette(id)
  recettes.splice(recettes.indexOf(recette), 1)

  setResponseStatus(event, 204)
})

Un id non numérique donne NaN, que trouverRecette ne trouve pas : tu obtiens un 404, ce qui reste cohérent.

L'appeler depuis une page

await $fetch('/api/recettes', { method: 'POST', body: { titre: 'Gaufres', duree: 30 } })
await $fetch('/api/recettes/2', { method: 'DELETE' })

Teste aussi les cas d'erreur avec curl : un corps vide doit renvoyer 400, un identifiant inconnu 404.

À retenir

  • Un CRUD, ce sont cinq routes : un fichier par méthode, avec [id] pour une recette précise.
  • Valide tout ce qui arrive (corps, paramètres) et ne reprends que les champs attendus.
  • Choisis le bon code : 201 à la création, 204 à la suppression, 400 pour une entrée invalide, 404 pour l'absent.
  • createError({ status, statusText }) interrompt la route avec une erreur HTTP.
  • Range le code partagé dans server/utils/ : il est importé automatiquement.

Pour aller plus loin

Sources officielles

Ensuite

Projet
Lignes de code
Niveau
Sous-sol
Appartement
API dans Nuxt
Pièce
5/7
Plan n°
api/05.ecrire-une-api-complete
Lignes de code · fiches rédigées à partir des documentations officielles, sources citées en bas de chaque page.