Écrire une API complète
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 :
| Fichier | Requête | Réponse |
|---|---|---|
server/api/recettes/index.get.ts | GET /api/recettes | 200, la liste |
server/api/recettes/index.post.ts | POST /api/recettes | 201, la recette créée |
server/api/recettes/[id].get.ts | GET /api/recettes/2 | 200 ou 404 |
server/api/recettes/[id].patch.ts | PATCH /api/recettes/2 | 200 ou 404 |
server/api/recettes/[id].delete.ts | DELETE /api/recettes/2 | 204 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 :
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
}
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 :
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 :
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
}
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
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.
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 :
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
Cacher une clé d'API
Pourquoi une clé d'API ne doit jamais atteindre le navigateur, et comment la garder côté serveur avec runtimeConfig et une route intermédiaire.
Sécurité et limites
Protéger une API : ne jamais faire confiance aux entrées, contrôler l'accès, régler CORS, mettre en cache et limiter les abus.
- Projet
- Lignes de code
- Niveau
- Sous-sol
- Appartement
- API dans Nuxt
- Pièce
- 5/7
- Plan n°
- api/05.ecrire-une-api-complete