18 - OpenAPI : le contrat entre ton API et le monde
Ce que tu vas apprendre
- Ce qu'est OpenAPI 3.x et la structure d'un fichier de spec
- La différence entre code-first et spec-first
- Comment utiliser $ref pour organiser une grosse spec
- La génération de types TypeScript avec openapi-typescript
- Les bonnes pratiques pour les examples et les descriptions
Prerequisites
Avoir lu l'article sur HATEOAS. Savoir lire du YAML et avoir une API REST a documenter.
Pendant des annees, j'ai documente mes APIs dans un fichier Notion. Trois colonnes : endpoint, méthode, description. A chaque modification de l'API, je mettais à jour le doc. Ou plutot, j'oubliais. En six mois, le doc et l'API n'avaient plus rien a voir. Le jour ou j'ai découvert OpenAPI, j'ai compris que la documentation d'une API ne devait pas etre un document séparé mais un contrat lie au code.
OpenAPI 3.x en bref
OpenAPI (anciennement Swagger) est une spécification qui decrit une API REST dans un format machine-readable (YAML ou JSON). Un fichier OpenAPI contient :
- Les endpoints disponibles (paths)
- Les méthodes HTTP supportees
- Les paramètres, headers, request bodies
- Les schemas de réponse
- L'authentification requise
yamlopenapi: 3.1.0
info:
title: Mon API
version: 1.0.0
description: API de gestion de produits
paths:
/products:
get:
summary: Liste des produits
operationId: listProducts
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
responses:
"200":
description: Liste paginee des produits
content:
application/json:
schema:
$ref: "#/components/schemas/ProductList"
Code-first vs spec-first
Spec-first : tu ecris la spec OpenAPI avant le code. L'équipe valide le contrat, puis les devs implementent. Le front peut commencer a coder avec un mock server généré depuis la spec.
Code-first : tu ecris le code et la spec est générée automatiquement depuis les decorateurs ou les annotations. Pratique, mais la spec devient un sous-produit du code au lieu d'etre un contrat explicite.
Mon avis : spec-first pour les API publiques et les équipes front/back separees. Code-first pour les projets solo ou les API internes ou la vitesse prime. Sur paltemps.fr, je fais du code-first avec Elysia qui généré la spec automatiquement.
En code-first avec Elysia :
typescriptimport { Elysia, t } from "elysia";
import { swagger } from "@elysiajs/swagger";
new Elysia()
.use(swagger())
.get("/products", () => db.select().from(products), {
query: t.Object({
page: t.Optional(t.Number({ default: 1 })),
limit: t.Optional(t.Number({ default: 20 }))
}),
response: t.Object({
data: t.Array(t.Object({
id: t.Number(),
name: t.String(),
price: t.Number()
}))
})
})
.listen(3000);
Elysia généré la spec OpenAPI a /swagger/json. Les types du schema sont verifies au runtime et utilises pour la documentation. Un seul endroit, zero desynchronisation.
Organiser avec $ref
Une spec monolithique de 3000 lignes est inmaintenable. $ref permet de découper en fichiers :
yaml# openapi.yaml
paths:
/products:
$ref: "./paths/products.yaml"
/orders:
$ref: "./paths/orders.yaml"
components:
schemas:
Product:
$ref: "./schemas/product.yaml"
Order:
$ref: "./schemas/order.yaml"
yaml# schemas/product.yaml
type: object
required:
- id
- name
- price
properties:
id:
type: integer
example: 42
name:
type: string
example: "Burger classique"
price:
type: integer
description: Prix en centimes
example: 1200
Chaque schema dans son fichier. Chaque path dans son fichier. La spec principale ne fait qu'assembler les morceaux. redocly bundle openapi.yaml -o dist/openapi.yaml les fusionne en un seul fichier pour la distribution.
Generer des types TypeScript
openapi-typescript transforme ta spec en types TypeScript. Fini les interfaces manuelles qui derivent :
bashnpx openapi-typescript ./openapi.yaml -o ./src/api-types.ts
Ca généré :
typescriptexport interface components {
schemas: {
Product: {
id: number;
name: string;
price: number;
};
ProductList: {
data: components["schemas"]["Product"][];
pagination: components["schemas"]["Pagination"];
};
};
}
Avec openapi-fetch, tu as un client type-safe généré depuis la spec :
typescriptimport createClient from "openapi-fetch";
import type { paths } from "./api-types";
const client = createClient<paths>({ baseUrl: "https://api.example.com" });
const { data } = await client.GET("/products", {
params: { query: { page: 1, limit: 20 } }
});
// data est type: { data: Product[], pagination: Pagination }
Zero any, zero cast. Si la spec change, le compilateur te dit exactement quoi corriger.
Les examples, c'est pas du luxe
Une spec sans examples est une spec que personne ne lit. Ajoute des examples partout :
yamlcomponents:
schemas:
Product:
type: object
properties:
name:
type: string
example: "Burger classique"
price:
type: integer
description: Prix en centimes
example: 1200
status:
type: string
enum: [draft, published, archived]
example: published
examples:
ProductExample:
summary: Un burger
value:
id: 42
name: "Burger classique"
price: 1200
status: published
Les examples apparaissent dans Swagger UI et Redoc. Ils permettent au lecteur de comprendre les donnees en un coup d'oeil, sans lire les descriptions de chaque champ.
Résumé
- OpenAPI 3.x est le standard pour decrire une API REST en YAML/JSON
- Spec-first pour les API publiques, code-first pour les projets internes
- Utilise $ref pour découper une grosse spec en fichiers gignables
- openapi-typescript + openapi-fetch donnent un client type-safe sans effort
- Ajoute des examples a chaque schema et chaque réponse
Article précédent : HATEOAS Article suivant : Documentation