Minisforum MS-01 i9-13900H : review après achat du bundle à €1189
J'aurais dû vérifier le thread count avant de lire les reviews. Parce que la quasi-totalité…
Les API REST (Representational State Transfer) sont devenues le standard de facto pour la communication entre applications web en 2025. Avec plus de 83% des APIs publiques utilisant REST, la maîtrise des design patterns et des meilleures pratiques n’est plus optionnelle. Ce guide complet vous accompagnera dans la conception d’APIs REST professionnelles, scalables et maintenables.
REST n’est pas un protocole mais un style architectural basé sur 6 contraintes :
POST /api/v1/users HTTP/1.1
Host: api.exemple.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Accept: application/json
User-Agent: MonApp/1.0
{
"username": "jdupont",
"email": "j.dupont@exemple.com",
"password": "SecureP@ss123"
}
Réponse :
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/users/12345
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1703001600
{
"id": 12345,
"username": "jdupont",
"email": "j.dupont@exemple.com",
"createdat": "2025-12-18T10:30:00Z",
"profileurl": "/api/v1/users/12345",
"links": {
"self": { "href": "/api/v1/users/12345" },
"posts": { "href": "/api/v1/users/12345/posts" }
}
}
MAUVAIS :
GET /getUsers
POST /createUser
POST /updateUser
POST /deleteUser
GET /getUserPosts?userId=123
BON :
GET /api/v1/users # Liste des utilisateurs
POST /api/v1/users # Créer un utilisateur
GET /api/v1/users/123 # Détails d'un utilisateur
PUT /api/v1/users/123 # Remplacer un utilisateur
PATCH /api/v1/users/123 # Modifier partiellement
DELETE /api/v1/users/123 # Supprimer un utilisateur
GET /api/v1/users/123/posts # Posts de l'utilisateur
POST /api/v1/users/123/posts # Créer un post pour l'utilisateur
// Express.js - API complète d'utilisateurs
const express = require('express');
const router = express.Router();
// GET - Récupérer (Idempotent & Safe)
router.get('/users', async (req, res) => {
try {
const { page = 1, limit = 20, sort = 'createdat', order = 'desc' } = req.query;
const users = await User.findAll({
limit: parseInt(limit),
offset: (page - 1) limit,
order: [[sort, order.toUpperCase()]]
});
const total = await User.count();
res.json({
data: users,
meta: {
currentpage: parseInt(page),
perpage: parseInt(limit),
total: total,
lastpage: Math.ceil(total / limit)
},
links: {
self: /api/v1/users?page=${page},
first: /api/v1/users?page=1,
last: /api/v1/users?page=${Math.ceil(total / limit)},
...(page > 1 && { prev: /api/v1/users?page=${parseInt(page) - 1} }),
...(page < Math.ceil(total / limit) && { next: /api/v1/users?page=${parseInt(page) + 1} })
}
});
} catch (error) {
res.status(500).json({ error: 'Internal server error', message: error.message });
}
});
// POST - Créer (Non-idempotent)
router.post('/users', async (req, res) => {
try {
const { username, email, password } = req.body;
// Validation
if (!username || !email || !password) {
return res.status(400).json({
error: 'Validation failed',
details: {
username: !username ? 'Username is required' : null,
email: !email ? 'Email is required' : null,
password: !password ? 'Password is required' : null
}
});
}
// Vérifier si l'utilisateur existe
const existingUser = await User.findOne({ where: { email } });
if (existingUser) {
return res.status(409).json({
error: 'Conflict',
message: 'User with this email already exists'
});
}
const user = await User.create({ username, email, password });
res.status(201)
.location(/api/v1/users/${user.id})
.json({
id: user.id,
username: user.username,
email: user.email,
createdat: user.createdat
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// PUT - Remplacer complètement (Idempotent)
router.put('/users/:id', async (req, res) => {
try {
const { id } = req.params;
const { username, email, password } = req.body;
const user = await User.findByPk(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// PUT remplace TOUTES les propriétés
await user.update({ username, email, password });
res.json({
id: user.id,
username: user.username,
email: user.email,
updatedat: user.updatedat
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// PATCH - Modifier partiellement (Idempotent)
router.patch('/users/:id', async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
const user = await User.findByPk(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
// PATCH modifie uniquement les champs fournis
await user.update(updates);
res.json({
id: user.id,
username: user.username,
email: user.email,
updatedat: user.updatedat
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// DELETE - Supprimer (Idempotent)
router.delete('/users/:id', async (req, res) => {
try {
const { id } = req.params;
const user = await User.findByPk(id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
await user.destroy();
res.status(204).send(); // No Content
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
module.exports = router;
Approche recommandée : URL versioning
// server.js
const express = require('express');
const app = express();
// Version 1
const v1Routes = require('./routes/v1');
app.use('/api/v1', v1Routes);
// Version 2 - Modifications non rétrocompatibles
const v2Routes = require('./routes/v2');
app.use('/api/v2', v2Routes);
// Rediriger /api vers la dernière version
app.use('/api', v2Routes);
Autres approches :
# Header versioning
GET /api/users HTTP/1.1
Accept: application/vnd.myapp.v2+json
# Query parameter
GET /api/users?version=2
# Custom header
GET /api/users HTTP/1.1
X-API-Version: 2
// routes/v1/products.js
router.get('/products', async (req, res) => {
try {
const {
// Pagination
page = 1,
limit = 20,
// Tri
sort = 'createdat',
order = 'desc',
// Filtrage
category,
minprice,
maxprice,
instock,
// Recherche
search,
// Sélection de champs
fields
} = req.query;
// Construction de la requête
const where = {};
if (category) {
where.category = category;
}
if (minprice || maxprice) {
where.price = {};
if (minprice) where.price[Op.gte] = parseFloat(minprice);
if (maxprice) where.price[Op.lte] = parseFloat(maxprice);
}
if (instock !== undefined) {
where.stock = instock === 'true' ? { [Op.gt]: 0 } : 0;
}
if (search) {
where[Op.or] = [
{ name: { [Op.like]: %${search}% } },
{ description: { [Op.like]: %${search}% } }
];
}
// Sélection de champs
const attributes = fields ? fields.split(',') : undefined;
const products = await Product.findAndCountAll({
where,
attributes,
limit: parseInt(limit),
offset: (page - 1) limit,
order: [[sort, order.toUpperCase()]]
});
res.json({
data: products.rows,
meta: {
currentpage: parseInt(page),
perpage: parseInt(limit),
total: products.count,
lastpage: Math.ceil(products.count / limit),
filtersapplied: {
category,
minprice,
maxprice,
instock,
search
}
}
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
Exemples d’utilisation :
# Filtrage basique
GET /api/v1/products?category=electronics&instock=true
# Filtrage par plage de prix
GET /api/v1/products?minprice=100&maxprice=500
# Recherche textuelle
GET /api/v1/products?search=laptop
# Tri
GET /api/v1/products?sort=price&order=asc
# Pagination
GET /api/v1/products?page=2&limit=50
# Sélection de champs (sparse fieldsets)
GET /api/v1/products?fields=id,name,price
# Combinaison
GET /api/v1/products?category=electronics&sort=price&order=asc&page=1&limit=20&fields=id,name,price
// middleware/errorHandler.js
class ApiError extends Error {
constructor(statusCode, message, details = null) {
super(message);
this.statusCode = statusCode;
this.details = details;
}
}
const errorHandler = (err, req, res, next) => {
// Logger l'erreur (production: utiliser Winston, Sentry, etc.)
console.error('Error:', err);
// Erreur personnalisée
if (err instanceof ApiError) {
return res.status(err.statusCode).json({
error: {
status: err.statusCode,
message: err.message,
...(err.details && { details: err.details }),
timestamp: new Date().toISOString(),
path: req.path
}
});
}
// Erreur de validation (express-validator)
if (err.array) {
return res.status(400).json({
error: {
status: 400,
message: 'Validation failed',
details: err.array(),
timestamp: new Date().toISOString(),
path: req.path
}
});
}
// Erreur de base de données
if (err.name === 'SequelizeValidationError') {
return res.status(400).json({
error: {
status: 400,
message: 'Database validation failed',
details: err.errors.map(e => ({ field: e.path, message: e.message })),
timestamp: new Date().toISOString(),
path: req.path
}
});
}
// Erreur générique
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
error: {
status: statusCode,
message: process.env.NODEENV === 'production'
? 'Internal server error'
: err.message,
...(process.env.NODEENV !== 'production' && { stack: err.stack }),
timestamp: new Date().toISOString(),
path: req.path
}
});
};
module.exports = { ApiError, errorHandler };
// routes/v1/orders.js
const { ApiError } = require('../../middleware/errorHandler');
router.post('/orders', async (req, res, next) => {
try {
const { userid, items } = req.body;
// 400 Bad Request - Validation échouée
if (!items || items.length === 0) {
throw new ApiError(400, 'Order must contain at least one item');
}
// 404 Not Found - Ressource inexistante
const user = await User.findByPk(userid);
if (!user) {
throw new ApiError(404, 'User not found');
}
// 409 Conflict - Conflit d'état
const pendingOrder = await Order.findOne({
where: { userid, status: 'pending' }
});
if (pendingOrder) {
throw new ApiError(409, 'User already has a pending order', {
existingorderid: pendingOrder.id
});
}
// 422 Unprocessable Entity - Données valides mais logique métier échouée
for (const item of items) {
const product = await Product.findByPk(item.productid);
if (product.stock < item.quantity) {
throw new ApiError(422, 'Insufficient stock', {
productid: product.id,
requested: item.quantity,
available: product.stock
});
}
}
const order = await Order.create({ userid, items });
// 201 Created - Ressource créée avec succès
res.status(201)
.location(/api/v1/orders/${order.id})
.json(order);
} catch (error) {
next(error);
}
});
// 204 No Content - Succès sans corps de réponse
router.delete('/orders/:id', async (req, res, next) => {
try {
const order = await Order.findByPk(req.params.id);
if (!order) {
throw new ApiError(404, 'Order not found');
}
await order.destroy();
res.status(204).send();
} catch (error) {
next(error);
}
});
// 503 Service Unavailable - Service temporairement indisponible
router.get('/external-data', async (req, res, next) => {
try {
const externalService = await fetch('https://external-api.com/data');
if (!externalService.ok) {
throw new ApiError(503, 'External service unavailable', {
retryafter: 60
});
}
const data = await externalService.json();
res.json(data);
} catch (error) {
next(error);
}
});
// utils/hateoas.js
const generateLinks = (resource, id, relationships = []) => {
const links = {
self: { href: /api/v1/${resource}/${id} }
};
relationships.forEach(rel => {
links[rel] = { href: /api/v1/${resource}/${id}/${rel} };
});
return links;
};
// routes/v1/users.js
router.get('/users/:id', async (req, res) => {
const user = await User.findByPk(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json({
id: user.id,
username: user.username,
email: user.email,
links: generateLinks('users', user.id, ['posts', 'comments', 'followers']),
actions: {
update: {
method: 'PATCH',
href: /api/v1/users/${user.id},
fields: ['username', 'email', 'bio']
},
delete: {
method: 'DELETE',
href: /api/v1/users/${user.id}
}
}
});
});
# main.py
from fastapi import FastAPI, HTTPException, Query, Path, status, Depends
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional, List
from datetime import datetime
from enum import Enum
import uvicorn
app = FastAPI(
title="API REST Professionnelle",
description="API REST avec meilleures pratiques 2025",
version="1.0.0",
docsurl="/api/docs",
redocurl="/api/redoc"
)
# CORS
app.addmiddleware(
CORSMiddleware,
alloworigins=[""],
allowcredentials=True,
allowmethods=[""],
allowheaders=[""],
)
# Modèles Pydantic
class UserRole(str, Enum):
ADMIN = "admin"
USER = "user"
MODERATOR = "moderator"
class UserCreate(BaseModel):
username: str = Field(..., minlength=3, maxlength=50)
email: EmailStr
password: str = Field(..., minlength=8)
role: UserRole = UserRole.USER
@validator('username')
def usernamealphanumeric(cls, v):
if not v.replace('', '').isalnum():
raise ValueError('Username must be alphanumeric')
return v
class UserUpdate(BaseModel):
username: Optional[str] = Field(None, minlength=3, maxlength=50)
email: Optional[EmailStr] = None
role: Optional[UserRole] = None
class UserResponse(BaseModel):
id: int
username: str
email: EmailStr
role: UserRole
createdat: datetime
updatedat: Optional[datetime]
class Config:
fromattributes = True
class PaginatedResponse(BaseModel):
data: List[UserResponse]
meta: dict
links: dict
class ErrorResponse(BaseModel):
error: dict
# Gestion des erreurs personnalisée
@app.exceptionhandler(HTTPException)
async def httpexceptionhandler(request, exc):
return JSONResponse(
statuscode=exc.statuscode,
content={
"error": {
"status": exc.statuscode,
"message": exc.detail,
"timestamp": datetime.now().isoformat(),
"path": str(request.url.path)
}
}
)
# Base de données simulée
usersdb = []
nextid = 1
# Endpoints
@app.get(
"/api/v1/users",
responsemodel=PaginatedResponse,
summary="Liste des utilisateurs",
description="Récupère une liste paginée d'utilisateurs avec filtres"
)
async def getusers(
page: int = Query(1, ge=1, description="Numéro de page"),
limit: int = Query(20, ge=1, le=100, description="Éléments par page"),
role: Optional[UserRole] = Query(None, description="Filtrer par rôle"),
search: Optional[str] = Query(None, description="Rechercher dans username/email")
):
# Filtrage
filteredusers = usersdb
if role:
filteredusers = [u for u in filteredusers if u.get('role') == role]
if search:
filteredusers = [
u for u in filteredusers
if search.lower() in u.get('username', '').lower()
or search.lower() in u.get('email', '').lower()
]
# Pagination
total = len(filteredusers)
start = (page - 1) limit
end = start + limit
paginatedusers = filteredusers[start:end]
return {
"data": paginatedusers,
"meta": {
"currentpage": page,
"perpage": limit,
"total": total,
"lastpage": (total + limit - 1) // limit
},
"links": {
"self": f"/api/v1/users?page={page}&limit={limit}",
"first": f"/api/v1/users?page=1&limit={limit}",
"last": f"/api/v1/users?page={(total + limit - 1) // limit}&limit={limit}"
}
}
@app.post(
"/api/v1/users",
responsemodel=UserResponse,
statuscode=status.HTTP201CREATED,
summary="Créer un utilisateur",
responses={
201: {"description": "Utilisateur créé"},
400: {"model": ErrorResponse, "description": "Validation échouée"},
409: {"model": ErrorResponse, "description": "Utilisateur existe déjà"}
}
)
async def createuser(user: UserCreate):
global nextid
# Vérifier si l'utilisateur existe
if any(u.get('email') == user.email for u in usersdb):
raise HTTPException(
statuscode=status.HTTP409CONFLICT,
detail="User with this email already exists"
)
newuser = {
"id": nextid,
"username": user.username,
"email": user.email,
"role": user.role,
"createdat": datetime.now(),
"updatedat": None
}
usersdb.append(newuser)
nextid += 1
return newuser
@app.get(
"/api/v1/users/{userid}",
responsemodel=UserResponse,
summary="Détails d'un utilisateur",
responses={
200: {"description": "Utilisateur trouvé"},
404: {"model": ErrorResponse, "description": "Utilisateur non trouvé"}
}
)
async def getuser(
userid: int = Path(..., ge=1, description="ID de l'utilisateur")
):
user = next((u for u in usersdb if u.get('id') == userid), None)
if not user:
raise HTTPException(
statuscode=status.HTTP404NOTFOUND,
detail=f"User with id {userid} not found"
)
return user
@app.patch(
"/api/v1/users/{userid}",
responsemodel=UserResponse,
summary="Modifier un utilisateur"
)
async def updateuser(
userid: int = Path(..., ge=1),
userupdate: UserUpdate = None
):
user = next((u for u in usersdb if u.get('id') == userid), None)
if not user:
raise HTTPException(
statuscode=status.HTTP404NOTFOUND,
detail=f"User with id {userid} not found"
)
# Mise à jour partielle
updatedata = userupdate.dict(excludeunset=True)
for field, value in updatedata.items():
user[field] = value
user['updatedat'] = datetime.now()
return user
@app.delete(
"/api/v1/users/{userid}",
statuscode=status.HTTP204NOCONTENT,
summary="Supprimer un utilisateur"
)
async def deleteuser(userid: int = Path(..., ge=1)):
global usersdb
user = next((u for u in usersdb if u.get('id') == userid), None)
if not user:
raise HTTPException(
statuscode=status.HTTP404NOTFOUND,
detail=f"User with id {userid} not found"
)
usersdb = [u for u in usersdb if u.get('id') != userid]
return None
# Health check
@app.get("/health")
async def healthcheck():
return {
"status": "healthy",
"timestamp": datetime.now().isoformat(),
"version": "1.0.0"
}
if name == "main":
uvicorn.run(app, host="0.0.0.0", port=8000)
# openapi.yaml
openapi: 3.0.3
info:
title: API REST Professionnelle
description: API REST avec design patterns et meilleures pratiques
version: 1.0.0
contact:
name: Support API
email: support@exemple.com
license:
name: MIT
url: https://opensource.org/licenses/MIT
servers:
- url: https://api.exemple.com/v1
description: Production
- url: https://staging-api.exemple.com/v1
description: Staging
- url: http://localhost:3000/v1
description: Development
tags:
- name: Users
description: Gestion des utilisateurs
- name: Products
description: Gestion des produits
paths:
/users:
get:
tags: [Users]
summary: Liste des utilisateurs
parameters:
- $ref: '#/components/parameters/PageParam'
- $ref: '#/components/parameters/LimitParam'
- name: role
in: query
schema:
type: string
enum: [admin, user, moderator]
responses:
'200':
description: Liste paginée
content:
application/json:
schema:
$ref: '#/components/schemas/PaginatedUsers'
post:
tags: [Users]
summary: Créer un utilisateur
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserCreate'
responses:
'201':
description: Utilisateur créé
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
$ref: '#/components/responses/BadRequest'
'409':
$ref: '#/components/responses/Conflict'
components:
schemas:
User:
type: object
properties:
id:
type: integer
example: 123
username:
type: string
example: jdupont
email:
type: string
format: email
example: j.dupont@exemple.com
role:
type: string
enum: [admin, user, moderator]
createdat:
type: string
format: date-time
UserCreate:
type: object
required:
- username
- email
- password
properties:
username:
type: string
minLength: 3
maxLength: 50
email:
type: string
format: email
password:
type: string
format: password
minLength: 8
Error:
type: object
properties:
error:
type: object
properties:
status:
type: integer
message:
type: string
timestamp:
type: string
format: date-time
PaginatedUsers:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
meta:
type: object
properties:
currentpage:
type: integer
perpage:
type: integer
total:
type: integer
lastpage:
type: integer
parameters:
PageParam:
name: page
in: query
schema:
type: integer
minimum: 1
default: 1
LimitParam:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
BadRequest:
description: Validation échouée
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
Conflict:
description: Conflit de ressource
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
security:
- bearerAuth: []
// tests/users.test.js
const request = require('supertest');
const app = require('../app');
describe('Users API', () => {
let userId;
// Test GET /users
describe('GET /api/v1/users', () => {
it('devrait retourner une liste paginée', async () => {
const res = await request(app)
.get('/api/v1/users')
.query({ page: 1, limit: 20 });
expect(res.statusCode).toBe(200);
expect(res.body).toHaveProperty('data');
expect(res.body).toHaveProperty('meta');
expect(res.body).toHaveProperty('links');
expect(Array.isArray(res.body.data)).toBe(true);
});
it('devrait filtrer par rôle', async () => {
const res = await request(app)
.get('/api/v1/users')
.query({ role: 'admin' });
expect(res.statusCode).toBe(200);
res.body.data.forEach(user => {
expect(user.role).toBe('admin');
});
});
it('devrait valider les paramètres de pagination', async () => {
const res = await request(app)
.get('/api/v1/users')
.query({ page: -1 });
expect(res.statusCode).toBe(400);
});
});
// Test POST /users
describe('POST /api/v1/users', () => {
it('devrait créer un utilisateur valide', async () => {
const newUser = {
username: 'testuser',
email: 'test@exemple.com',
password: 'SecureP@ss123'
};
const res = await request(app)
.post('/api/v1/users')
.send(newUser);
expect(res.statusCode).toBe(201);
expect(res.body).toHaveProperty('id');
expect(res.body.username).toBe(newUser.username);
expect(res.body).not.toHaveProperty('password');
expect(res.headers.location).toMatch(//api/v1/users/d+/);
userId = res.body.id;
});
it('devrait rejeter un email invalide', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({
username: 'testuser',
email: 'invalid-email',
password: 'SecureP@ss123'
});
expect(res.statusCode).toBe(400);
expect(res.body.error.details).toBeDefined();
});
it('devrait rejeter un mot de passe trop court', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({
username: 'testuser',
email: 'test2@exemple.com',
password: '123'
});
expect(res.statusCode).toBe(400);
});
it('devrait rejeter un email déjà existant', async () => {
const res = await request(app)
.post('/api/v1/users')
.send({
username: 'testuser2',
email: 'test@exemple.com',
password: 'SecureP@ss123'
});
expect(res.statusCode).toBe(409);
});
});
// Test PATCH /users/:id
describe('PATCH /api/v1/users/:id', () => {
it('devrait modifier partiellement un utilisateur', async () => {
const res = await request(app)
.patch(/api/v1/users/${userId})
.send({ username: 'updateduser' });
expect(res.statusCode).toBe(200);
expect(res.body.username).toBe('updateduser');
expect(res.body.updatedat).toBeDefined();
});
it('devrait retourner 404 pour un ID inexistant', async () => {
const res = await request(app)
.patch('/api/v1/users/99999')
.send({ username: 'test' });
expect(res.statusCode).toBe(404);
});
});
// Test DELETE /users/:id
describe('DELETE /api/v1/users/:id', () => {
it('devrait supprimer un utilisateur', async () => {
const res = await request(app)
.delete(/api/v1/users/${userId});
expect(res.statusCode).toBe(204);
expect(res.body).toEqual({});
});
it('devrait retourner 404 après suppression', async () => {
const res = await request(app)
.get(/api/v1/users/${userId});
expect(res.statusCode).toBe(404);
});
});
});
{
"info": {
"name": "API REST Tests",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"item": [
{
"name": "Users",
"item": [
{
"name": "Créer utilisateur",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status 201', () => {",
" pm.response.to.have.status(201);",
"});",
"",
"pm.test('Retourne un ID', () => {",
" const json = pm.response.json();",
" pm.expect(json).to.have.property('id');",
" pm.environment.set('userid', json.id);",
"});",
"",
"pm.test('Header Location présent', () => {",
" pm.response.to.have.header('Location');",
"});"
]
}
}
],
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{n "username": "{{$randomUserName}}",n "email": "{{$randomEmail}}",n "password": "SecureP@ss123"n}"
},
"url": {
"raw": "{{baseurl}}/api/v1/users",
"host": ["{{baseurl}}"],
"path": ["api", "v1", "users"]
}
}
},
{
"name": "Récupérer utilisateur",
"event": [
{
"listen": "test",
"script": {
"exec": [
"pm.test('Status 200', () => {",
" pm.response.to.have.status(200);",
"});",
"",
"pm.test('Temps de réponse < 500ms', () => {",
" pm.expect(pm.response.responseTime).to.be.below(500);",
"});",
"",
"pm.test('Structure de réponse valide', () => {",
" const schema = {",
" type: 'object',",
" required: ['id', 'username', 'email'],",
" properties: {",
" id: { type: 'number' },",
" username: { type: 'string' },",
" email: { type: 'string', format: 'email' }",
" }",
" };",
" pm.response.to.have.jsonSchema(schema);",
"});"
]
}
}
],
"request": {
"method": "GET",
"url": {
"raw": "{{baseurl}}/api/v1/users/{{userid}}",
"host": ["{{baseurl}}"],
"path": ["api", "v1", "users", "{{user_id}}"]
}
}
}
]
}
]
}
La conception d’une API REST professionnelle en 2025 nécessite une attention particulière à :
Ces pratiques garantissent une API scalable, maintenable et facile à consommer.
Mots-clés: REST API, design patterns API, meilleures pratiques API, Express.js, FastAPI, OpenAPI, Swagger, pagination API, filtrage API, versioning API, gestion erreurs API, tests API, HATEOAS
Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.