Conception d'API pour des systemes qui survivent a ton equipe
Comment concevoir des API qui survivent au turnover. Protobuf comme source de verite, tRPC pour les stacks TypeScript, GraphQL en production, gestion des breaking changes et contrats d'erreur.
L'API qui survit a son createur
Chaque API demarre avec une equipe, un client et un cas d'usage. Deux ans plus tard, 5 equipes en dependent, 3 partenaires externes s'y integrent, et les developpeurs originaux sont partis. Les decisions de conception de l'API sont devenues permanentes.
Nous avons construit des API avec trois strategies differentes : Protobuf comme source de verite pour les systemes multi-langages, tRPC pour les stacks 100% TypeScript, et GraphQL pour les besoins clients complexes. Chacune etait le bon choix dans son contexte. Cet article couvre quand utiliser chacune et comment concevoir des API qui survivent au turnover.
Pour comprendre comment ces API s'integrent dans une architecture plus large, consulte notre guide d'architecture systeme et notre guide d'ingenierie logicielle.
Protobuf : source de verite pour les systemes multi-langages
Quand ton systeme couvre plusieurs langages (API TypeScript, service de recherche Go, pipeline ML Python), tu as besoin d'un format de schema qui genere des types pour chacun d'eux. Protobuf fait exactement ca.
// proto/product.proto
syntax = "proto3";
package commerce.v1;
message Product {
string id = 1;
string name = 2;
string description = 3;
int32 price_cents = 4;
string currency = 5;
repeated string category_ids = 6;
ProductStatus status = 7;
google.protobuf.Timestamp created_at = 8;
}
enum ProductStatus {
PRODUCT_STATUS_UNSPECIFIED = 0;
PRODUCT_STATUS_DRAFT = 1;
PRODUCT_STATUS_ACTIVE = 2;
PRODUCT_STATUS_ARCHIVED = 3;
}
service ProductService {
rpc GetProduct(GetProductRequest) returns (Product);
rpc ListProducts(ListProductsRequest) returns (ListProductsResponse);
rpc CreateProduct(CreateProductRequest) returns (Product);
}
Pourquoi les tags comptent pour l'evolution
Les champs Protobuf sont identifies par des numeros de tag (1, 2, 3...), pas par leur nom. Ca veut dire que tu peux renommer un champ sans casser les clients existants. Tu peux ajouter de nouveaux champs (nouveaux numeros de tag) sans casser les anciens clients. Tu peux deprecier des champs sans les supprimer.
// Evolution securisee : ajout d'un champ
message Product {
string id = 1;
string name = 2;
string description = 3;
int32 price_cents = 4;
string currency = 5;
repeated string category_ids = 6;
ProductStatus status = 7;
google.protobuf.Timestamp created_at = 8;
string sku = 9; // NOUVEAU : ajoute sans casser les clients existants
string brand = 10; // NOUVEAU : les anciens clients ignorent les champs inconnus
}
Regles pour une evolution Protobuf securisee :
- Ne reutilise jamais un numero de tag (meme apres avoir supprime un champ)
- Ne change jamais le type d'un champ
- Ajoute les nouveaux champs avec de nouveaux numeros de tag
- Marque les champs deprecies avec
reservedpour eviter la reutilisation accidentelle
Generation de code
# Genere TypeScript, Go et Python a partir du meme fichier .proto
protoc --ts_out=./gen/ts --go_out=./gen/go --python_out=./gen/py proto/*.proto
Un seul fichier de schema genere des clients et des serveurs type-safe dans chaque langage. Les modifications de schema passent par une pull request. La generation de code est une etape CI. Les incoherences de types sont des erreurs de compilation, pas des bugs runtime.
tRPC : quand tout ton stack est TypeScript
Si ton serveur API et tous tes clients sont en TypeScript, tRPC elimine completement la couche de schema. Les types circulent du serveur au client a la compilation. Pas de generation de code, pas de fichiers de schema, pas de spec OpenAPI.
// Serveur : definition du router avec des procedures typees
import { router, publicProcedure, protectedProcedure } from './trpc';
import { z } from 'zod';
export const productRouter = router({
list: publicProcedure
.input(z.object({
cursor: z.string().optional(),
limit: z.number().min(1).max(100).default(20),
category: z.string().optional(),
}))
.query(async ({ input, ctx }) => {
return ctx.productService.list(input);
}),
create: protectedProcedure
.input(z.object({
name: z.string().min(1).max(200),
price: z.number().positive(),
description: z.string().optional(),
}))
.mutation(async ({ input, ctx }) => {
return ctx.productService.create(ctx.tenantId, input);
}),
});
// Client : inference de types complete, pas de generation de code
const products = await trpc.product.list.query({
limit: 10,
category: 'electronics',
});
// products est entierement type : { items: Product[], nextCursor?: string }
Quand tRPC fonctionne le mieux
- Tous les clients sont en TypeScript (app web, React Native, services Node.js)
- L'API est interne (pas exposee a des tiers)
- L'iteration rapide compte plus que les contrats d'API formels
- L'equipe est petite et co-localisee (les changements serveur et client se font ensemble)
Quand tRPC ne fonctionne pas
- Des partenaires externes doivent s'integrer (ils ont besoin de docs OpenAPI/Swagger)
- Les clients sont dans d'autres langages (mobile natif, Go, Python)
- Tu as besoin de versionnement d'API pour la retro-compatibilite
- L'API est un produit public
Nous utilisons tRPC avec Hono pour les API TypeScript internes. Consulte notre guide des backends TypeScript pour la comparaison complete des stacks.
GraphQL : pour les besoins clients complexes
GraphQL brille quand les clients ont besoin de requetes flexibles : differentes pages necessitent differents sous-ensembles de donnees, le mobile a besoin de moins de donnees que le web, et l'equipe frontend veut iterer sur les requetes sans modifications backend.
# Le client demande exactement ce dont il a besoin
query ProductPage($slug: String!) {
product(slug: $slug) {
id
name
price
images { url alt }
reviews(first: 5, status: APPROVED) {
items { rating body customerName }
totalItems
}
relatedProducts(first: 4) {
id name price images { url }
}
}
}
Patterns GraphQL en production
Requetes persistees : N'autorise pas les requetes arbitraires en production. Les clients envoient un hash de requete, le serveur cherche la requete dans un registre. Ca empeche les abus de requetes et permet le caching.
Limitation de profondeur : Sans limites, un client peut envoyer une requete profondement imbriquee qui fait des jointures sur toutes les tables de ta base de donnees.
apiOptions: {
middleware: [depthLimit(10)],
shopApiPlayground: false, // Desactive en production
}
Prevention du N+1 : Utilise DataLoader pour regrouper les requetes en base de donnees. Sans ca, une requete qui recupere 20 produits avec leurs categories fait 20 requetes de categories separees.
// DataLoader regroupe N requetes individuelles en 1
const categoryLoader = new DataLoader(async (ids: string[]) => {
const categories = await categoryRepo.findByIds(ids);
return ids.map(id => categories.find(c => c.id === id));
});
Analyse de complexite : Assigne un cout a chaque champ. Rejette les requetes qui depassent un budget de complexite.
Pour voir comment nous utilisons GraphQL dans Vendure commerce, consulte notre guide de production Vendure.
Breaking changes : evolution par tags vs versionnement par URL
| Strategie | Comment ca fonctionne | Ideal pour |
|---|---|---|
| Par tags (Protobuf) | Ajoute des champs avec de nouveaux tags, les anciens clients les ignorent | Multi-langages, gRPC |
Versionnement par URL (/v1/, /v2/) | Endpoints separes par version | API REST avec consommateurs externes |
Versionnement par header (Accept: application/vnd.api.v2+json) | Meme URL, version dans le header | API REST avec des URLs propres |
| GraphQL (pas de versionnement) | Ajoute des champs, deprecie les anciens avec @deprecated | API GraphQL |
| tRPC (pas de versionnement) | Les types evoluent avec la codebase | API TypeScript internes |
Pour la plupart des API internes, evite le versionnement par URL. Ca double ta surface de maintenance. Ajoute de nouveaux champs, deprecie les anciens, supprime-les une fois que tous les clients ont migre.
Pour les API externes (integrations tiers, API publiques), le versionnement par URL est plus sur parce que tu ne controles pas quand les clients mettent a jour.
Contrats d'erreur
Les erreurs font partie du contrat d'API. Les clients ont besoin d'erreurs structurees sur lesquelles ils peuvent agir, pas de messages texte qu'ils parsent avec des regex.
// Reponse d'erreur structuree
interface ApiError {
code: string; // Lisible par machine : "PRODUCT_NOT_FOUND", "INSUFFICIENT_STOCK"
message: string; // Lisible par humain : "Product with ID xyz not found"
details?: object; // Contexte supplementaire pour le debug
requestId: string; // ID de correlation pour le support
}
// Exemples de reponses
// 404
{
"code": "PRODUCT_NOT_FOUND",
"message": "Product with ID prod_123 not found",
"requestId": "req_abc456"
}
// 409
{
"code": "INSUFFICIENT_STOCK",
"message": "Only 2 units available, requested 5",
"details": { "available": 2, "requested": 5 },
"requestId": "req_def789"
}
// 422
{
"code": "VALIDATION_ERROR",
"message": "Invalid input",
"details": {
"fields": [
{ "field": "price", "error": "must be positive" },
{ "field": "name", "error": "must not be empty" }
]
},
"requestId": "req_ghi012"
}
Le champ code est le contrat. Les clients font un switch dessus. Le message est pour les humains. Les details fournissent du contexte. Le requestId permet au support de retrouver la requete exacte dans les logs.
Gouvernance d'API
Au fur et a mesure que l'API grandit, tu as besoin de gouvernance pour eviter les incoherences :
| Regle | Pourquoi |
|---|---|
| Tous les endpoints necessitent une authentification | Pas d'endpoints publics accidentels |
| Toutes les mutations necessitent des cles d'idempotence | Les retries reseau ne creent pas de doublons |
Toutes les reponses incluent un requestId | Chaque requete est tracable |
| Toutes les erreurs utilisent le format structure | Les clients peuvent gerer les erreurs de maniere programmatique |
| Tous les endpoints de liste supportent la pagination | Pas de requetes non bornees |
| Les breaking changes necessitent une review | Une seule personne ne peut pas casser tous les consommateurs |
| Les depreciations necessitent un calendrier de migration | "Deprecie" sans deadline signifie "jamais supprime" |
Erreurs courantes
-
REST vs GraphQL comme identite. Choisis en fonction des besoins clients, pas de la preference de l'equipe. REST pour du CRUD simple avec des consommateurs externes. GraphQL pour des requetes flexibles avec les equipes frontend. tRPC pour du TypeScript interne. Protobuf pour du multi-langages.
-
Pas de contrat d'erreur. Des messages d'erreur textuels qui changent a chaque release cassent chaque client qui essaie de les gerer.
-
Endpoints de liste non bornes. Un endpoint qui retourne les 50 000 produits en une seule reponse va planter les clients et surcharger ta base de donnees. La pagination est obligatoire.
-
Versionner les API internes. Si tu controles tous les clients, fais evoluer l'API en place. Le versionnement double la maintenance sans benefice.
-
Pas de calendrier de depreciation. Marquer un champ
@deprecatedsans date de suppression signifie qu'il reste pour toujours. Fixe une date, notifie les consommateurs, supprime-le. -
GraphQL sans limites de profondeur. Une API GraphQL sans restrictions est un vecteur de deni de service. Limite la profondeur, la complexite et le cout des requetes.
Points cles a retenir
-
Protobuf pour les systemes multi-langages. Evolution par tags, generation de code, efficacite du format wire. Le schema est le contrat.
-
tRPC pour les stacks 100% TypeScript. Zero generation de code, inference de types complete, vitesse d'iteration maximale. Mais ca ne fonctionne que quand tous les clients sont en TypeScript.
-
GraphQL pour les besoins clients flexibles. Les clients requetent exactement ce dont ils ont besoin. Mais ajoute des limites de profondeur, des requetes persistees et une analyse de complexite pour la production.
-
Les contrats d'erreur sont aussi importants que les contrats de succes. Des codes lisibles par machine, des messages lisibles par humain, des ID de correlation et des details structures.
-
Evolue en place pour les API internes, versionne pour les externes. Ajouter des champs est safe. Supprimer des champs necessite un calendrier de migration. Le versionnement par URL est un dernier recours.
Nous concevons des API dans le cadre de nos pratiques de developpement web et de logiciel sur mesure. Si tu as besoin d'aide pour l'architecture de tes API, parle a notre equipe ou demande un devis.
Sujets couverts
Guides connexes
Guide Entreprise des Systèmes d'IA Agentiques
Guide technique des systemes d'IA agentiques en entreprise. Decouvre l'architecture, les capacites et les applications des agents IA autonomes.
Lire le guideCommerce Agentique : Comment laisser les agents IA acheter en toute securite
Comment concevoir un commerce agentique gouverne. Moteurs de politiques, portes d'approbation HITL, reçus HMAC, idempotence, isolation multi-tenant et le protocole Agentic Checkout complet.
Lire le guideLes 9 endroits où ton système IA laisse fuir des données (et comment colmater chacun)
Cartographie systématique de chaque point de fuite de données dans les systèmes IA. Prompts, embeddings, logs, appels d'outils, mémoire d'agent, messages d'erreur, cache, données de fine-tuning et transferts entre agents.
Lire le guidePrêt à construire des systèmes IA prêts pour la production ?
Notre équipe est spécialisée dans les systèmes IA prêts pour la production. Discutons de comment nous pouvons aider.
Démarrer une conversation