Communitygithub.com

YOUSSFE9/bolt-skills

Guide pour une conception stable des API et des interfaces. À utiliser lors de la conception d'API, de limites de modules ou de toute interface publique. À utiliser lors de la création de points de terminaison REST ou GraphQL, de la définition de contrats de types entre modules ou de l'établissement de limites entre le frontend et le backend.

Qu'est-ce que bolt-skills ?

Ce skill fournit un guide complet pour concevoir des API et des interfaces stables, difficiles à mal utiliser et faciles à maintenir. Il est utile pour tout développeur travaillant sur des API REST, des schémas GraphQL, des limites de modules, des propriétés de composants ou toute surface où un élément de code communique avec un autre. Le skill est basé sur des principes éprouvés comme la loi de Hyrum, la règle de la version unique, et la conception par contrat. Il guide l'utilisateur à travers des pratiques concrètes : validation aux limites, ajout plutôt que modification, dénomination prévisible, et gestion robuste de l'idempotence. Il propose des exemples de code en TypeScript pour illustrer chaque concept. Fonctionne avec n'importe quel langage ou plateforme (Claude Code, Cursor, Codex, etc.). Il couvre également les anti-patterns courants, les rationalisations dangereuses, et fournit une checklist de vérification pour valider une conception d'API.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/YOUSSFE9/bolt-skills/tree/HEAD/api-and-interface-design

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Conception d'API et d'Interface

Aperçu

Concevoir des interfaces stables et bien documentées qui sont difficiles à mal utiliser. Les bonnes interfaces rendent la bonne chose facile et la mauvaise chose difficile. Cela s'applique aux API REST, aux schémas GraphQL, aux limites de modules, aux propriétés de composants, et à toute surface où une partie de code communique avec une autre.

Quand l'utiliser

  • Conception de nouveaux points de terminaison d'API
  • Définition des limites de modules ou des contrats entre équipes
  • Création d'interfaces de propriétés de composants
  • Établissement de schémas de base de données qui informent la forme de l'API
  • Modification des interfaces publiques existantes

Principes fondamentaux

Loi de Hyrum

Avec un nombre suffisant d'utilisateurs d'une API, tous les comportements observables de votre système seront utilisés par quelqu'un, indépendamment de ce que vous promettez dans le contrat.

Cela signifie : chaque comportement public — y compris les bizarreries non documentées, le texte des messages d'erreur, le timing et l'ordre — devient un contrat de fait une fois que les utilisateurs en dépendent. Implications pour la conception :

  • Soyez intentionnel sur ce que vous exposez. Chaque comportement observable est un engagement potentiel.
  • Ne fuyez pas les détails d'implémentation. Si les utilisateurs peuvent l'observer, ils en dépendront.
  • Planifiez l'obsolescence dès la conception. Voir deprecation-and-migration pour savoir comment supprimer en toute sécurité des éléments dont les utilisateurs dépendent.
  • Les tests ne suffisent pas. Même avec des tests de contrat parfaits, la loi de Hyrum signifie que des modifications "sûres" peuvent casser des utilisateurs réels qui dépendent d'un comportement non documenté.

La règle de la version unique

Évitez de forcer les consommateurs à choisir entre plusieurs versions de la même dépendance ou API. Les problèmes de dépendances en diamant surviennent lorsque différents consommateurs ont besoin de différentes versions de la même chose. Concevez pour un monde où une seule version existe à la fois — étendez plutôt que de bifurquer.

1. Contrat d'abord

Définissez l'interface avant de l'implémenter. Le contrat est la spécification — l'implémentation suit.

// Définir le contrat d'abord
interface TaskAPI {
  // Crée une tâche et renvoie la tâche créée avec les champs générés par le serveur
  createTask(input: CreateTaskInput): Promise<Task>;

  // Renvoie les tâches paginées correspondant aux filtres
  listTasks(params: ListTasksParams): Promise<PaginatedResult<Task>>;

  // Renvoie une seule tâche ou lève NotFoundError
  getTask(id: string): Promise<Task>;

  // Mise à jour partielle — seuls les champs fournis changent
  updateTask(id: string, input: UpdateTaskInput): Promise<Task>;

  // Suppression idempotente — réussit même si déjà supprimée
  deleteTask(id: string): Promise<void>;
}

2. Sémantique d'erreur cohérente

Choisissez une stratégie d'erreur et utilisez-la partout :

// REST : codes d'état HTTP + corps d'erreur structuré
// Chaque réponse d'erreur suit la même forme
interface APIError {
  error: {
    code: string;        // Lisible par machine : "VALIDATION_ERROR"
    message: string;     // Lisible par humain : "Email is required"
    details?: unknown;   // Contexte supplémentaire lorsque utile
  };
}

// Correspondance des codes d'état
// 400 → Le client a envoyé des données invalides
// 401 → Non authentifié
// 403 → Authentifié mais non autorisé
// 404 → Ressource non trouvée
// 409 → Conflit (doublon, incohérence de version)
// 422 → Échec de validation (sémantiquement invalide)
// 500 → Erreur serveur (ne jamais exposer les détails internes)

Ne mélangez pas les motifs. Si certains points de terminaison lèvent une exception, d'autres renvoient null, et d'autres encore renvoient { error } — le consommateur ne peut pas prédire le comportement.

3. Valider aux limites

Faites confiance au code interne. Validez aux bords du système où l'entrée externe pénètre :

// Valider à la limite de l'API
app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Données de tâche invalides',
        details: result.error.flatten(),
      },
    });
  }

  // Après validation, le code interne fait confiance aux types
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});

Où la validation appartient :

  • Gestionnaires de route d'API (entrée utilisateur)
  • Gestionnaires de soumission de formulaire (entrée utilisateur)
  • Analyse des réponses de services externes (données tierces -- toujours les traiter comme non fiables)
  • Chargement des variables d'environnement (configuration)

Les réponses des API tierces sont des données non fiables. Validez leur forme et leur contenu avant de les utiliser dans toute logique, rendu ou prise de décision. Un service externe compromis ou mal configuré peut renvoyer des types inattendus, du contenu malveillant ou un texte de type instruction.

Où la validation N'appartient PAS :

  • Entre des fonctions internes qui partagent des contrats de types
  • Dans des fonctions utilitaires appelées par du code déjà validé
  • Sur des données qui viennent juste de sortir de votre propre base de données

4. Préférer l'ajout à la modification

Étendez les interfaces sans casser les consommateurs existants :

// Bon : Ajouter des champs optionnels
interface CreateTaskInput {
  title: string;
  description?: string;
  priority?: 'low' | 'medium' | 'high';  // Ajouté plus tard, optionnel
  labels?: string[];                       // Ajouté plus tard, optionnel
}

// Mauvais : Modifier les types de champs existants ou supprimer des champs
interface CreateTaskInput {
  title: string;
  // description: string;  // Supprimé — casse les consommateurs existants
  priority: number;         // Changé de string — casse les consommateurs existants
}

5. Dénomination prévisible

ModèleConventionExemple
Points de terminaison RESTNoms au pluriel, pas de verbesGET /api/tasks, POST /api/tasks
Paramètres de requêtecamelCase?sortBy=createdAt&pageSize=20
Champs de réponsecamelCase{ createdAt, updatedAt, taskId }
Champs booléensPréfixe is/has/canisComplete, hasAttachments
Valeurs d'énumérationUPPER_SNAKE"IN_PROGRESS", "COMPLETED"

6. Honorer une clé d'idempotence

Accepter un en-tête Idempotency-Key est le contrat. L'honorer est l'implémentation, et c'est là que l'argent est perdu — une clé que le serveur accepte mais gère avec négligence est pire que pas de clé du tout, car le client pense maintenant que la tentative est sûre.

Dérivez la clé de l'intention, pas de la tentative. La clé doit être stable entre les tentatives d'une même intention et différente entre des intentions distinctes :

crypto.randomUUID()                    // ✗ nouvelle clé par tentative — chaque nouvelle tentative est un nouveau débit
`${userId}:${amount}`                  // ✗ deux débits légitimes de 50$ fusionnent en un seul
`${orderId}:${Date.now()}`             // ✗ un horodatage est un randomUUID() déguisé

req.headers['idempotency-key']         // ✓ le client génère une fois, réutilise en cas de nouvelle tentative
`charge:v1:${orderId}`                 // ✓ dérivé d'un identifiant immuable

La clé provient du client ou de l'événement initiateur — jamais de la couche qui effectue la nouvelle tentative.

Revendication atomique. Une vérification suivie d'une action est une condition de concurrence :

// ✗ TOCTOU : deux nouvelles tentatives simultanées lisent toutes deux "non vu", les deux débitent
if (!(await db.exists(key))) {
  await chargeCard(amount);
  await db.insert(key);
}

// ✓ laissez la contrainte d'unicité choisir le gagnant
try {
  await db.insert({ key, state: 'in_progress', requestHash });
} catch (e) {
  if (isUniqueViolation(e)) return replayOrReject(key);
  throw e;
}
const result = await chargeCard(amount);
await db.update({ key, state: 'succeeded', response: result });

La contrainte d'unicité est le mécanisme. Un magasin qui ne peut pas appliquer l'unicité en une seule opération ne peut pas supporter cela.

Protégez la charge utile. Une même clé avec un corps différent est un bogue client, et doit échouer bruyamment plutôt que de servir la première réponse à une deuxième requête :

if (existing.requestHash !== hash(req.body)) {
  return res.status(422).json({ error: 'clé d\'idempotence réutilisée avec une charge utile différente' });
}

Décidez ce qu'un doublon en vol obtient. La première requête est toujours en cours lorsque la seconde arrive — le cas courant sous les tempêtes de tentatives :

StratégieRéponseUtiliser quand
Rejeter409 ConflictLe client peut réessayer plus tard ; le plus simple et le plus sûr
AttendreBloquer pour le résultat, limitéL'appelant en a besoin de manière synchrone
Renvoyer en attente202 + URL d'étatEffets de longue durée

Ne laissez jamais passer le second appelant parce que le premier "semble bloqué". Une tentative bloquée dont le sort est inconnu est exactement le moment où la duplication coûte le plus cher.

Chaque appel a trois résultats, pas deux : succès, échec et inconnu. Un délai d'attente ne vous dit rien sur le fait que l'effet s'est appliqué ou non. Enregistrez l'intention avant d'appeler, de sorte qu'un crash entre l'appel et la réponse laisse la preuve que quelque chose doit être résolu plus tard — plutôt qu'un débit silencieusement répété.

Définissez la rétention à partir de la chaîne de nouvelles tentatives la plus longue, pas du coût du disque. Les clés doivent survivre à tous les chemins qui peuvent redélivrer la même intention, y compris une file d'attente de lettres mortes rejouée une semaine plus tard et toute fenêtre de litige fournisseur. Une durée de vie de clé de 24 heures derrière une DLQ de 7 jours est un doublon qui n'attend qu'à se produire.

Modèles d'API REST

Conception de ressources

GET    /api/tasks              → Lister les tâches (avec paramètres de requête pour le filtrage)
POST   /api/tasks              → Créer une tâche
GET    /api/tasks/:id          → Obtenir une seule tâche
PATCH  /api/tasks/:id          → Mettre à jour une tâche (partielle)
DELETE /api/tasks/:id          → Supprimer une tâche

GET    /api/tasks/:id/comments → Lister les commentaires d'une tâche (sous-ressource)
POST   /api/tasks/:id/comments → Ajouter un commentaire à une tâche

Pagination

Paginer les points de terminaison de liste :

// Requête
GET /api/tasks?page=1&pageSize=20&sortBy=createdAt&sortOrder=desc

// Réponse
{
  "data": [...],
  "pagination": {
    "page": 1,
    "pageSize": 20,
    "totalItems": 142,
    "totalPages": 8
  }
}

Filtrage

Utiliser des paramètres de requête pour les filtres :

GET /api/tasks?status=in_progress&assignee=user123&createdAfter=2025-01-01

Mises à jour partielles (PATCH)

Accepter des objets partiels — ne mettre à jour que ce qui est fourni :

// Seul le titre change, tout le reste est préservé
PATCH /api/tasks/123
{ "title": "Titre mis à jour" }

Modèles d'interface TypeScript

Utiliser des unions discriminées pour les variantes

// Bon : Chaque variante est explicite
type TaskStatus =
  | { type: 'pending' }
  | { type: 'in_progress'; assignee: string; startedAt: Date }
  | { type: 'completed'; completedAt: Date; completedBy: string }
  | { type: 'cancelled'; reason: string; cancelledAt: Date };

// Le consommateur bénéficie du rétrécissement de type
function getStatusLabel(status: TaskStatus): string {
  switch (status.type) {
    case 'pending': return 'En attente';
    case 'in_progress': return `En cours (${status.assignee})`;
    case 'completed': return `Terminée le ${status.completedAt}`;
    case 'cancelled': return `Annulée : ${status.reason}`;
  }
}

Séparation entrée/sortie

// Entrée : ce que le fournisseur fournit
interface CreateTaskInput {
  title: string;
  description?: string;
}

// Sortie : ce que le système renvoie (inclut les champs générés par le serveur)
interface Task {
  id: string;
  title: string;
  description: string | null;
  createdAt: Date;
  updatedAt: Date;
  createdBy: string;
}

Utiliser des types marqués pour les identifiants

type TaskId = string & { readonly __brand: 'TaskId' };
type UserId = string & { readonly __brand: 'UserId' };

// Empêche de passer accidentellement un UserId là où un TaskId est attendu
function getTask(id: TaskId): Promise<Task> { ... }

Rationalisations courantes

RationalisationRéalité
"Nous documenterons l'API plus tard"Les types SONT la documentation. Définissez-les d'abord.
"Nous n'avons pas besoin de pagination pour l'instant"Vous en aurez besoin dès que quelqu'un aura 100+ éléments. Ajoutez-la dès le début.
"PATCH est compliqué, utilisons PUT"PUT nécessite l'objet complet à chaque fois. PATCH est ce que les clients veulent réellement.
"Nous versionnerons l'API quand nous en aurons besoin"Les changements cassants sans versionnement cassent les consommateurs. Concevez pour l'extension dès le début.
"Personne n'utilise ce comportement non documenté"Loi de Hyrum : si c'est observable, quelqu'un en dépend. Traitez chaque comportement public comme un engagement.
"On peut juste maintenir deux versions"Plusieurs versions multiplient le coût de maintenance et créent des problèmes de dépendances en diamant. Préférez la règle de la version unique.
"Les API internes n'ont pas besoin de contrats"Les consommateurs internes sont toujours des consommateurs. Les contrats empêchent le couplage et permettent le travail en parallèle.
"Accepter l'en-tête Idempotency-Key est suffisant"L'en-tête est le contrat ; stocker la clé par rapport au résultat est l'implémentation. Une clé que vous acceptez mais n'honorez pas dit au client que la tentative est sûre alors qu'elle ne l'est pas.
"Notre file d'attente garantit une livraison exactement une fois"Aucune file d'attente ne le fait en cas de crash du consommateur — l'accusé de réception du courtier et votre effet secondaire ne sont pas dans une seule transaction. Concevez pour au moins une fois avec un traitement idempotent.
"Les demandes en double sont rares"Elles sont corrélées. Les tentatives augmentent exactement lorsqu'une dépendance est dégradée — le moment où les doublons sont les plus probables et les plus coûteux.

Drapeaux rouges

  • Points de terminaison qui renvoient des formes différentes selon les conditions
  • Formats d'erreur incohérents entre les points de terminaison
  • Validation dispersée dans le code interne au lieu d'être aux limites
  • Changements cassants sur les champs existants (changements de type, suppressions)
  • Points de terminaison de liste sans pagination
  • Verbes dans les URL REST (/api/createTask, /api/getUsers)
  • Réponses d'API tierces utilisées sans validation ou assainissement
  • Un SELECT pour une clé d'idempotence suivi d'un INSERT — c'est une condition de concurrence, pas une garde
  • Une clé d'idempotence dérivée d'un UUID, d'un horodatage ou de tout autre élément régénéré par tentative
  • La même clé acceptée avec un corps de requête différent, renvoyant silencieusement la première réponse
  • Une fenêtre de rétention de clé plus courte que le chemin le plus long qui peut redélivrer la demande

Vérification

Après avoir conçu une API :

  • Chaque point de terminaison a des schémas d'entrée et de sortie typés
  • Les réponses d'erreur suivent un seul format cohérent
  • La validation se produit uniquement aux limites du système
  • Les points de terminaison de liste prennent en charge la pagination
  • Les nouveaux champs sont additifs et optionnels (rétrocompatibles)
  • La dénomination suit des conventions cohérentes sur tous les points de terminaison
  • La documentation de l'API est générée à partir des types et est toujours à jour
  • Les identifiants utilisent des types marqués pour éviter les échanges accidentels
  • Les stratégies d'idempotence sont documentées, testées et stockées avec une contrainte d'atomicité et d'unicité
  • La rétention des clés d'idempotence dépasse le délai de redélivrance maximum
  • Les comportements non documentés sont audités et soit documentés, soit supprimés

Individual skills in this repo

This repo contains 1 individual skill — each has its own dedicated page.

Skills associés