Ajouter une route API
Définir un endpoint sous /lrvo/api avec son niveau d'authentification, sa permission et ses réponses normalisées.
Toutes les routes HTTP de la plateforme passent par core/api, sous le préfixe /lrvo/api. Ajouter une route ne demande jamais de modifier le routeur.
Depuis un module
Définir les routes dans un fichier api.server.js, puis les enregistrer depuis register-server.js.
// src/api.server.js
import { defineApiRoutes, apiSuccess, apiError } from '@lrvo/core/api';
import { create, find, findById } from '@lrvo/core/database';
async function handleListPosts(_request, _params, _context) {
const posts = await find('lrvo_blog_posts', {}, { orderBy: 'created_at DESC', limit: 50 });
return apiSuccess({ posts });
}
async function handleCreatePost(request, _params, { session }) {
const body = await request.json();
if (!body.title?.trim()) {
return apiError('Bad Request', 'Le titre est requis.');
}
const post = await create('lrvo_blog_posts', {
...body,
author_id: session.user.id,
}, { allowedColumns: ['title', 'slug', 'content'] });
return apiSuccess({ success: true, post, message: 'Billet créé.' });
}
async function handleGetPost(_request, params) {
const post = await findById('lrvo_blog_posts', params.id);
if (!post) return apiError('Not Found', 'Billet introuvable.');
return apiSuccess({ post });
}
export const routes = defineApiRoutes([
{ path: '/blog/posts', method: 'GET', handler: handleListPosts, auth: 'admin', permission: 'blog.view' },
{ path: '/blog/posts', method: 'POST', handler: handleCreatePost, auth: 'admin', permission: 'blog.manage' },
{ path: '/blog/posts/:id', method: 'GET', handler: handleGetPost, auth: 'public' },
]);// src/register-server.js
import { registerApiRoutes } from '@lrvo/core/api';
import { routes } from './api.server.js';
registerApiRoutes(routes);Les routes sont disponibles à GET /lrvo/api/blog/posts, POST /lrvo/api/blog/posts et GET /lrvo/api/blog/posts/:id.
Depuis une feature du core
Même forme, enregistrée par registerFeatureRoutes dans initializeLrvo().
import { registerFeatureRoutes } from '@lrvo/core/api';
import { routes as settingsRoutes } from '../../features/settings/api.js';
registerFeatureRoutes(settingsRoutes);core-routes.js n'est utilisé que pour les routes internes de l'infrastructure, comme le health check et le stockage. Une feature n'y touche pas.
Déclarer l'authentification
L'authentification se déclare dans la définition de route, jamais dans le handler.
auth | Qui peut appeler | context.session |
|---|---|---|
'public' | Tout le monde | undefined |
'user' | Session valide | { user } |
'admin' | Utilisateur avec admin.access | { user } |
Un handler qui vérifierait lui-même les droits créerait une seconde source de vérité. Elle finira par diverger de la première, et c'est la version la plus permissive qui gagnera.
Sur une route auth: 'admin', le champ permission ajoute une exigence granulaire :
{ path: '/blog/posts', method: 'DELETE', handler: handleDelete, auth: 'admin', permission: 'blog.manage' }Un utilisateur admin sans blog.manage reçoit un 403 avant que le handler ne s'exécute.
Motifs de chemin
| Motif | Exemple | params |
|---|---|---|
| Exact | /blog/posts | {} |
| Paramètre | /blog/posts/:id | { id: '42' } |
| Wildcard | /blog/files/** | { wildcard: 'a/b/c.pdf' } |
Le wildcard n'est valide qu'en fin de motif.
Signature du handler
async function handler(request, params, context) {
// request — objet Request de Next.js
// params — paramètres extraits du chemin
// context — { session? }, présent dès que auth !== 'public'
}Un handler écrit avec la signature (request, params) continue de fonctionner : les arguments supplémentaires sont ignorés.
Réponses
Toujours retourner apiSuccess ou apiError. Le mapping vers un code HTTP est centralisé.
return apiSuccess({ posts }); // lecture
return apiSuccess({ success: true, post, message: 'Billet créé.' }); // mutation
return apiError('Not Found', 'Billet introuvable.');| Code | Statut |
|---|---|
'Unauthorized' | 401 |
'Admin access required' | 403 |
'Forbidden' | 403 |
'Not Found' | 404 |
'Bad Request' | 400 |
'Too Many Requests' | 429 |
'Internal Server Error' | 500 |
Le second argument est lu par l'utilisateur : il est en français, il nomme ce qui ne va pas, et il ne contient jamais de trace d'exécution, de nom de table ni de fragment SQL. Voir Rédaction.
Valider les entrées
La validation se fait en entrée de handler, côté serveur. Ne jamais supposer que le client a validé.
const body = await request.json();
if (!body.title?.trim()) return apiError('Bad Request', 'Le titre est requis.');Pour une écriture en base, passer une liste blanche de colonnes plutôt que d'étaler le corps de la requête :
await create('lrvo_blog_posts', body, { allowedColumns: ['title', 'slug', 'content'] });Sans elle, un champ non prévu envoyé par le client peut être écrit. Voir Sécurité.
Erreurs de configuration
defineApiRoutes valide les définitions à l'import, donc au démarrage du serveur. Un method invalide, un auth inconnu ou un handler absent lèvent immédiatement une TypeError explicite, plutôt que de produire un 404 silencieux en production.
Ce qui est appliqué automatiquement
Avant chaque handler, le routeur applique :
- La limitation de débit par identité, sauf sur une route
skipRateLimit. - La protection CSRF sur les méthodes mutantes, sauf sur une route
skipCsrf. - Le niveau d'authentification déclaré.
- La permission granulaire, si elle est déclarée.
Le préréglage api autorise 300 requêtes par minute et par adresse IP, et le triple, 900, pour un appelant connecté, dont le compteur est tenu sur son compte et non sur son adresse. Voir Rate limit.
skipCsrf est réservé aux appels machine à machine, qui n'envoient ni Origin ni Referer et prouvent leur identité autrement. Le webhook Stripe du core en est le seul usage : il vérifie la signature du corps avant d'agir. Sans cette vérification dans le handler, la route serait une écriture ouverte à tous.
Ce qui ne passe pas par le routeur
Le contenu public. Une page publique est rendue côté serveur, elle ne s'appelle pas elle-même en HTTP. Voir Frontières serveur/client.