API Conventions
Purpose
Standards pour développer les API REST dans le projet consultant-manager.
Principes REST
Ressources
- •Consultant:
/api/consultants - •Mission:
/api/missions - •Dashboard:
/api/dashboard
Méthodes HTTP
- •
GET- Récupérer des données (lecture) - •
POST- Créer une nouvelle ressource - •
PUT- Modifier une ressource existante (remplacer) - •
PATCH- Modifier partiellement une ressource - •
DELETE- Supprimer une ressource
URLs
code
GET /api/consultants # Liste GET /api/consultants/:id # Détail POST /api/consultants # Créer PUT /api/consultants/:id # Modifier DELETE /api/consultants/:id # Supprimer GET /api/missions GET /api/missions/:id POST /api/missions PUT /api/missions/:id DELETE /api/missions/:id GET /api/missions/timeline # Endpoint spécial GET /api/dashboard/stats # Endpoint agrégé
Status Codes HTTP
Success
- •
200 OK- GET, PUT réussis - •
201 Created- POST réussi - •
204 No Content- DELETE réussi
Client Errors
- •
400 Bad Request- Données invalides - •
401 Unauthorized- Non authentifié - •
403 Forbidden- Non autorisé - •
404 Not Found- Ressource introuvable - •
409 Conflict- Conflit (ex: email déjà utilisé) - •
422 Unprocessable Entity- Validation échouée
Server Errors
- •
500 Internal Server Error- Erreur serveur
Format de Réponse
Success Response
json
// GET /api/consultants/:id
{
"id": "uuid",
"nom": "Dupont",
"prenom": "Jean",
"email": "jean@example.com",
"telephone": "+33612345678",
"competences": ["React", "TypeScript"],
"tjm": 500,
"statut": "EN_MISSION",
"dateCreation": "2026-01-15T10:00:00.000Z",
"dateModification": "2026-01-15T10:00:00.000Z"
}
Error Response
json
{
"error": "Message d'erreur lisible",
"details": [
{
"field": "email",
"message": "Email invalide"
}
]
}
Liste Paginée
json
{
"data": [...],
"pagination": {
"page": 1,
"pageSize": 20,
"total": 150,
"totalPages": 8
}
}
Query Parameters
Filtres
code
GET /api/consultants?statut=DISPONIBLE GET /api/consultants?search=dupont GET /api/missions?consultantId=uuid GET /api/missions?dateDebut=2026-01-01&dateFin=2026-12-31
Tri
code
GET /api/consultants?sortBy=nom&order=asc GET /api/missions?sortBy=dateDebut&order=desc
Pagination
code
GET /api/consultants?page=2&pageSize=20
Include Relations
code
GET /api/consultants/:id?include=missions
Validation
Backend avec Zod
typescript
import { z } from 'zod';
const consultantSchema = z.object({
nom: z.string().min(1, 'Nom requis'),
prenom: z.string().min(1, 'Prénom requis'),
email: z.string().email('Email invalide'),
telephone: z.string().optional(),
competences: z.array(z.string()).min(1, 'Au moins une compétence'),
tjm: z.number().positive('TJM doit être positif'),
statut: z.enum(['DISPONIBLE', 'EN_MISSION', 'EN_CONGES', 'INDISPONIBLE']).optional()
});
export const createConsultant = async (req: Request, res: Response) => {
try {
const validatedData = consultantSchema.parse(req.body);
const consultant = await prisma.consultant.create({
data: {
...validatedData,
competences: JSON.stringify(validatedData.competences)
}
});
res.status(201).json(consultant);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation error',
details: error.errors
});
}
res.status(500).json({ error: 'Internal server error' });
}
};
Gestion d'Erreurs
Error Handler Middleware
typescript
// Express error handler
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
// Prisma errors
if (err.code === 'P2002') {
return res.status(409).json({
error: 'Unique constraint violation',
field: err.meta?.target
});
}
if (err.code === 'P2025') {
return res.status(404).json({
error: 'Resource not found'
});
}
// Zod validation errors
if (err instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation error',
details: err.errors
});
}
// Generic error
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message
});
});
Try-Catch Pattern
typescript
export const getConsultant = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const consultant = await prisma.consultant.findUnique({
where: { id },
include: { missions: true }
});
if (!consultant) {
return res.status(404).json({ error: 'Consultant not found' });
}
res.json(consultant);
} catch (error) {
console.error('Error fetching consultant:', error);
res.status(500).json({ error: 'Failed to fetch consultant' });
}
};
Dates
Format ISO 8601
json
{
"dateDebut": "2026-01-15T00:00:00.000Z",
"dateFin": "2026-06-30T23:59:59.999Z"
}
Parsing
typescript
// Backend: convertir string en Date
const dateDebut = new Date(req.body.dateDebut);
// Valider avec Zod
const missionSchema = z.object({
dateDebut: z.string().datetime(),
dateFin: z.string().datetime()
}).refine(data => {
return new Date(data.dateFin) > new Date(data.dateDebut);
}, {
message: 'dateFin must be after dateDebut'
});
CORS
Configuration
typescript
import cors from 'cors';
// Development: permissif
app.use(cors());
// Production: restrictif
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true
}));
Sécurité
Validation Stricte
typescript
// ✅ Valider toutes les entrées
const validatedData = schema.parse(req.body);
// ❌ Utiliser directement req.body
await prisma.consultant.create({ data: req.body });
Rate Limiting
typescript
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use('/api/', limiter);
Helmet (Headers de sécurité)
typescript
import helmet from 'helmet'; app.use(helmet());
Documentation
JSDoc sur Routes
typescript
/**
* GET /api/consultants
*
* Liste tous les consultants avec filtres optionnels
*
* Query params:
* - statut (optional): DISPONIBLE | EN_MISSION | EN_CONGES | INDISPONIBLE
* - search (optional): Recherche par nom, prénom, email
*
* Returns: Consultant[]
*/
export const getAllConsultants = async (req: Request, res: Response) => {
// ...
};
Swagger/OpenAPI (Futur)
yaml
paths:
/api/consultants:
get:
summary: Liste des consultants
parameters:
- name: statut
in: query
schema:
type: string
enum: [DISPONIBLE, EN_MISSION, EN_CONGES, INDISPONIBLE]
responses:
200:
description: Liste des consultants
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Consultant'
Patterns Spécifiques au Projet
Calculs Côté Backend
typescript
// ✅ Calculs métier dans l'API
export const getMission = async (req: Request, res: Response) => {
const mission = await prisma.mission.findUnique({ where: { id } });
res.json({
...mission,
revenusGeneres: calculateRevenue(mission.tjm, mission.dateDebut, mission.dateFin),
dureeJours: calculateDuration(mission.dateDebut, mission.dateFin)
});
};
// ❌ Laisser le calcul au frontend
// Frontend ne devrait PAS calculer les revenus
JSON Fields
typescript
// Consultant.competences est stocké en JSON string
export const getConsultant = async (req: Request, res: Response) => {
const consultant = await prisma.consultant.findUnique({ where: { id } });
res.json({
...consultant,
competences: JSON.parse(consultant.competences) // Convertir en array
});
};
Statut Calculé
typescript
// Calculer le statut réel basé sur missions actives
export const getAllConsultants = async (req: Request, res: Response) => {
const consultants = await prisma.consultant.findMany({
include: { missions: true }
});
const consultantsWithStatus = consultants.map(consultant => ({
...consultant,
statut: calculateConsultantStatus(consultant) // Fonction utilitaire
}));
res.json(consultantsWithStatus);
};
Testing API
Avec curl
bash
# GET
curl http://localhost:3000/api/consultants
# POST
curl -X POST http://localhost:3000/api/consultants \
-H "Content-Type: application/json" \
-d '{
"nom": "Dupont",
"prenom": "Jean",
"email": "jean@example.com",
"competences": ["React"],
"tjm": 500
}'
# DELETE
curl -X DELETE http://localhost:3000/api/consultants/uuid
Avec Postman/Insomnia
Créer une collection avec tous les endpoints du projet.
Tests d'Intégration
typescript
import request from 'supertest';
import { app } from '../src/index';
describe('Consultants API', () => {
it('should create a consultant', async () => {
const response = await request(app)
.post('/api/consultants')
.send({
nom: 'Test',
prenom: 'User',
email: 'test@example.com',
competences: ['React'],
tjm: 500
})
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.email).toBe('test@example.com');
});
});
Checklist API
Avant de commiter une nouvelle API:
- • Routes RESTful (GET, POST, PUT, DELETE)
- • Validation Zod sur toutes les entrées
- • Status codes HTTP appropriés
- • Gestion d'erreurs avec try-catch
- • Réponses JSON consistantes
- • CORS configuré
- • Dates en ISO 8601
- • Calculs métier côté backend
- • Documentation JSDoc
- • Tests d'intégration
- • Pas de données sensibles dans les logs