AgentSkillsCN

api-conventions

REST API设计标准。适用于“api”、“endpoint”、“route”、“rest api”、“http”等场景。

SKILL.md
--- frontmatter
name: api-conventions
description: Standards pour la conception d'API REST. Use when "api", "endpoint", "route", "rest api", "http".
allowed-tools: Read, Grep, Glob

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

Ressources