api-contracts-generator
Génère des contrats API cohérents entre Frontend (Next.js) et Backend (NestJS) avec types synchronisés, validation standardisée et error handling uniforme. À utiliser lors de la création d'APIs, DTOs, types frontend/backend, ou quand l'utilisateur mentionne "API", "DTO", "types", "contract", "validation", "frontend-backend", "synchronisation".
About api-contracts-generator
api-contracts-generator is a Claude AI skill developed by RomualdP. Génère des contrats API cohérents entre Frontend (Next.js) et Backend (NestJS) avec types synchronisés, validation standardisée et error handling uniforme. À utiliser lors de la création d'APIs, DTOs, types frontend/backend, ou quand l'utilisateur mentionne "API", "DTO", "types", "contract", "validation", "frontend-backend", "synchronisation". This powerful Claude Code plugin helps developers automate workflows and enhance productivity with intelligent AI assistance.
Why use api-contracts-generator? With 0 stars on GitHub, this skill has been trusted by developers worldwide. Install this Claude skill instantly to enhance your development workflow with AI-powered automation.
| name | API Contracts Generator |
| description | Génère des contrats API cohérents entre Frontend (Next.js) et Backend (NestJS) avec types synchronisés, validation standardisée et error handling uniforme. À utiliser lors de la création d'APIs, DTOs, types frontend/backend, ou quand l'utilisateur mentionne "API", "DTO", "types", "contract", "validation", "frontend-backend", "synchronisation". |
| allowed-tools | ["Read","Write","Edit","Glob","Grep","Bash"] |
API Contracts Generator
🎯 Mission
Garantir une communication parfaite entre Frontend (Next.js) et Backend (NestJS) via des contrats API cohérents, types synchronisés et validation standardisée.
🏗️ Philosophie des API Contracts
Le Problème
Dans un projet full-stack, les erreurs de communication Frontend ↔ Backend sont fréquentes :
- ❌ Types incohérents (backend attend
clubId, frontend envoieid) - ❌ Validations divergentes (backend accepte 100 chars, frontend 50)
- ❌ Erreurs non standardisées (format différent selon l'endpoint)
- ❌ Documentation obsolète (Swagger non à jour)
La Solution : API Contracts
Un API Contract définit le contrat entre frontend et backend :
- ✅ DTOs Backend : Structure des requêtes/réponses avec validation
- ✅ Types Frontend : TypeScript synchronisés avec le backend
- ✅ Validation cohérente : Mêmes règles backend et frontend
- ✅ Error format standard : Format uniforme pour toutes les erreurs
- ✅ Documentation auto : Swagger généré depuis le code
Architecture de Communication
Frontend (Next.js)
↓ Server Action (avec types)
↓ Validation Zod
↓ fetch/axios
Backend (NestJS)
↓ Controller (avec DTOs)
↓ Validation class-validator
↓ Handler (CQRS)
↓ Response DTO
↑ JSON Response
Frontend (Next.js)
↑ Typed Response
↑ UI Update
📦 1. Backend DTOs (NestJS)
Request DTOs (Input)
Les Request DTOs définissent la structure des données envoyées par le frontend.
Template Request DTO
// volley-app-backend/src/club-management/presentation/dtos/create-club.dto.ts import { IsString, IsNotEmpty, IsOptional, MaxLength, MinLength } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class CreateClubDto { @ApiProperty({ description: 'Club name', example: 'Volley Club Paris', minLength: 3, maxLength: 100, }) @IsString() @IsNotEmpty() @MinLength(3) @MaxLength(100) readonly name: string; @ApiPropertyOptional({ description: 'Club description', example: 'Best volleyball club in Paris', maxLength: 500, }) @IsString() @IsOptional() @MaxLength(500) readonly description?: string; }
Règles pour Request DTOs :
- ✅ Validation avec
class-validator(IsString, IsNotEmpty, etc.) - ✅ Swagger decorators
@ApiPropertypour documentation - ✅
readonlypour immutabilité - ✅ Types primitifs (string, number, boolean, Date)
- ✅ Exemples dans Swagger (
example) - ❌ JAMAIS de logique métier (seulement validation)
Response DTOs (Output)
Les Response DTOs définissent la structure des données retournées par le backend.
Template Response DTO
// volley-app-backend/src/club-management/presentation/dtos/club-detail.dto.ts import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; export class OwnerDto { @ApiProperty({ example: 'user-123' }) id: string; @ApiProperty({ example: 'John Doe' }) name: string; @ApiProperty({ example: 'john@example.com' }) email: string; } export class SubscriptionDto { @ApiProperty({ example: 'FREE', enum: ['FREE', 'PRO', 'UNLIMITED'] }) plan: string; @ApiProperty({ example: 'ACTIVE', enum: ['ACTIVE', 'INACTIVE', 'EXPIRED'] }) status: string; @ApiProperty({ example: 1 }) maxTeams: number; @ApiProperty({ example: 0 }) currentTeamsCount: number; } export class ClubDetailDto { @ApiProperty({ example: 'club-123' }) id: string; @ApiProperty({ example: 'Volley Club Paris' }) name: string; @ApiPropertyOptional({ example: 'Best club in Paris' }) description?: string; @ApiProperty({ type: OwnerDto }) owner: OwnerDto; @ApiProperty({ type: SubscriptionDto }) subscription: SubscriptionDto; @ApiProperty({ example: 15 }) membersCount: number; @ApiProperty({ example: '2024-01-01T00:00:00.000Z' }) createdAt: Date; }
Règles pour Response DTOs :
- ✅ Swagger decorators pour documentation complète
- ✅ Nested DTOs pour relations (OwnerDto, SubscriptionDto)
- ✅ Exemples réalistes
- ✅ Enum values documentés
- ✅ Types primitifs + nested objects
- ❌ JAMAIS d'entités domain brutes (utiliser des mappers)
Pagination DTO (Standard)
// volley-app-backend/src/shared/dtos/pagination.dto.ts import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsInt, IsOptional, Max, Min } from 'class-validator'; export class PaginationQueryDto { @ApiPropertyOptional({ default: 1, minimum: 1 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) page?: number = 1; @ApiPropertyOptional({ default: 10, minimum: 1, maximum: 100 }) @IsOptional() @Type(() => Number) @IsInt() @Min(1) @Max(100) limit?: number = 10; } export class PaginationMetaDto { @ApiProperty({ example: 1 }) page: number; @ApiProperty({ example: 10 }) limit: number; @ApiProperty({ example: 50 }) total: number; @ApiProperty({ example: 5 }) totalPages: number; } export class PaginatedResponseDto<T> { @ApiProperty({ isArray: true }) data: T[]; @ApiProperty({ type: PaginationMetaDto }) meta: PaginationMetaDto; }
Controller Integration
// volley-app-backend/src/club-management/presentation/controllers/clubs.controller.ts import { Controller, Post, Get, Body, Param, Query, UseGuards } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger'; import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; import { CreateClubDto } from '../dtos/create-club.dto'; import { ClubDetailDto } from '../dtos/club-detail.dto'; import { ClubListDto } from '../dtos/club-list.dto'; import { PaginationQueryDto, PaginatedResponseDto } from '../../shared/dtos/pagination.dto'; import { CreateClubHandler } from '../../application/commands/create-club/create-club.handler'; import { GetClubHandler } from '../../application/queries/get-club/get-club.handler'; import { ListClubsHandler } from '../../application/queries/list-clubs/list-clubs.handler'; @ApiTags('Clubs') @ApiBearerAuth() @Controller('clubs') @UseGuards(JwtAuthGuard) export class ClubsController { constructor( private readonly createClubHandler: CreateClubHandler, private readonly getClubHandler: GetClubHandler, private readonly listClubsHandler: ListClubsHandler, ) {} @Post() @ApiOperation({ summary: 'Create a new club' }) @ApiResponse({ status: 201, description: 'Club created', type: String }) @ApiResponse({ status: 400, description: 'Validation error' }) async create(@Body() dto: CreateClubDto): Promise<{ id: string }> { const command = new CreateClubCommand(dto.name, dto.description, 'current-user-id'); const id = await this.createClubHandler.execute(command); return { id }; } @Get(':id') @ApiOperation({ summary: 'Get club details' }) @ApiResponse({ status: 200, description: 'Club found', type: ClubDetailDto }) @ApiResponse({ status: 404, description: 'Club not found' }) async findOne(@Param('id') id: string): Promise<ClubDetailDto> { const query = new GetClubQuery(id); return this.getClubHandler.execute(query); } @Get() @ApiOperation({ summary: 'List clubs with pagination' }) @ApiResponse({ status: 200, description: 'Clubs list', type: PaginatedResponseDto }) async findAll(@Query() pagination: PaginationQueryDto): Promise<PaginatedResponseDto<ClubListDto>> { const query = new ListClubsQuery(pagination.page, pagination.limit); return this.listClubsHandler.execute(query); } }
🎨 2. Frontend Types (Next.js)
Stratégie de Synchronisation
Option 1 : Générer les types depuis Swagger (Recommandé)
# Install openapi-typescript npm install --save-dev openapi-typescript # Generate types from backend Swagger npx openapi-typescript http://localhost:3000/api-json -o src/types/api.ts
Option 2 : Partager les types (Monorepo)
// shared/types/club.types.ts (partagé entre frontend et backend) export interface CreateClubInput { name: string; description?: string; } export interface ClubDetail { id: string; name: string; description?: string; owner: { id: string; name: string; email: string; }; subscription: { plan: string; status: string; maxTeams: number; currentTeamsCount: number; }; membersCount: number; createdAt: Date; }
Option 3 : Dupliquer les types manuellement (Moins recommandé)
// volley-app-frontend/src/features/club-management/types/club.types.ts // Dupliqué depuis backend CreateClubDto export interface CreateClubInput { name: string; description?: string; } // Dupliqué depuis backend ClubDetailDto export interface ClubDetail { id: string; name: string; description?: string; owner: { id: string; name: string; email: string; }; subscription: { plan: string; status: string; maxTeams: number; currentTeamsCount: number; }; membersCount: number; createdAt: Date; }
Validation Frontend avec Zod
// volley-app-frontend/src/features/club-management/schemas/club.schema.ts import { z } from 'zod'; // Schema SYNCHRONISÉ avec backend CreateClubDto export const createClubSchema = z.object({ name: z .string() .min(3, 'Le nom doit contenir au moins 3 caractères') .max(100, 'Le nom ne peut pas dépasser 100 caractères'), description: z .string() .max(500, 'La description ne peut pas dépasser 500 caractères') .optional(), }); export type CreateClubInput = z.infer<typeof createClubSchema>;
CRITIQUE : Les règles de validation Zod doivent EXACTEMENT correspondre aux règles backend (class-validator).
🔗 3. Server Actions (Frontend → Backend)
Template Server Action
// volley-app-frontend/src/features/club-management/actions/create-club.action.ts 'use server'; import { revalidatePath } from 'next/cache'; import { createClubSchema, CreateClubInput } from '../schemas/club.schema'; import { clubsApi } from '../api/clubs.api'; export async function createClubAction(input: CreateClubInput) { try { // 1. Validate input (frontend validation) const validated = createClubSchema.parse(input); // 2. Call backend API const response = await clubsApi.create(validated); // 3. Revalidate cache revalidatePath('/dashboard/coach'); // 4. Return success return { success: true as const, data: response, }; } catch (error) { // 5. Handle errors if (error instanceof z.ZodError) { return { success: false as const, error: { code: 'VALIDATION_ERROR', message: 'Données invalides', details: error.errors, }, }; } return { success: false as const, error: { code: 'UNKNOWN_ERROR', message: error.message || 'Une erreur est survenue', }, }; } } // Type du retour export type CreateClubResult = | { success: true; data: { id: string } } | { success: false; error: { code: string; message: string; details?: any } };
API Client
// volley-app-frontend/src/features/club-management/api/clubs.api.ts import { CreateClubInput, ClubDetail, ClubList } from '../types/club.types'; import { PaginatedResponse } from '@/types/api.types'; const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; export const clubsApi = { async create(input: CreateClubInput): Promise<{ id: string }> { const response = await fetch(`${API_BASE_URL}/clubs`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${getToken()}`, // Helper to get JWT }, body: JSON.stringify(input), }); if (!response.ok) { throw await handleApiError(response); } return response.json(); }, async getById(id: string): Promise<ClubDetail> { const response = await fetch(`${API_BASE_URL}/clubs/${id}`, { headers: { Authorization: `Bearer ${getToken()}`, }, }); if (!response.ok) { throw await handleApiError(response); } return response.json(); }, async list(page: number = 1, limit: number = 10): Promise<PaginatedResponse<ClubList>> { const response = await fetch( `${API_BASE_URL}/clubs?page=${page}&limit=${limit}`, { headers: { Authorization: `Bearer ${getToken()}`, }, }, ); if (!response.ok) { throw await handleApiError(response); } return response.json(); }, }; // Helper functions function getToken(): string { // Get JWT from cookies or localStorage return ''; } async function handleApiError(response: Response): Promise<Error> { const error = await response.json(); return new ApiError(error.code, error.message, error.details); } class ApiError extends Error { constructor( public code: string, message: string, public details?: any, ) { super(message); this.name = 'ApiError'; } }
⚠️ 4. Error Handling Standard
Backend Error Format
// volley-app-backend/src/shared/filters/http-exception.filter.ts import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common'; import { Response } from 'express'; export interface ErrorResponse { code: string; message: string; details?: any; timestamp: string; path: string; } @Catch() export class HttpExceptionFilter implements ExceptionFilter { catch(exception: unknown, host: ArgumentsHost) { const ctx = host.switchToHttp(); const response = ctx.getResponse<Response>(); const request = ctx.getRequest(); let status = HttpStatus.INTERNAL_SERVER_ERROR; let errorResponse: ErrorResponse = { code: 'INTERNAL_SERVER_ERROR', message: 'Une erreur interne est survenue', timestamp: new Date().toISOString(), path: request.url, }; if (exception instanceof HttpException) { status = exception.getStatus(); const exceptionResponse = exception.getResponse(); if (typeof exceptionResponse === 'object') { errorResponse = { ...errorResponse, ...(exceptionResponse as any), }; } else { errorResponse.message = exceptionResponse as string; } } response.status(status).json(errorResponse); } }
Frontend Error Handling
// volley-app-frontend/src/lib/api-error.ts export class ApiError extends Error { constructor( public code: string, message: string, public details?: any, public status?: number, ) { super(message); this.name = 'ApiError'; } static fromResponse(response: any): ApiError { return new ApiError( response.code || 'UNKNOWN_ERROR', response.message || 'Une erreur est survenue', response.details, response.status, ); } // User-friendly messages getUserMessage(): string { const messages: Record<string, string> = { VALIDATION_ERROR: 'Les données fournies sont invalides', NOT_FOUND: 'La ressource demandée n\'existe pas', UNAUTHORIZED: 'Vous devez être connecté pour effectuer cette action', FORBIDDEN: 'Vous n\'avez pas les permissions nécessaires', INTERNAL_SERVER_ERROR: 'Une erreur interne est survenue. Veuillez réessayer.', }; return messages[this.code] || this.message; } }
✅ 5. Checklist API Contract
Backend (NestJS)
- Request DTOs avec validation class-validator
- Response DTOs avec Swagger decorators
- Exemples réalistes dans Swagger
- Error handling standardisé
- Pagination DTO pour listes
- Swagger activé et accessible (
/api)
Frontend (Next.js)
- Types synchronisés avec backend (OpenAPI ou partagés)
- Validation Zod cohérente avec backend
- Server Actions avec types
- API client avec types
- Error handling standardisé
- Messages d'erreur traduits pour UI
Synchronisation
- Script de génération des types (si OpenAPI)
- CI/CD vérifie la synchronisation
- Documentation Swagger à jour
- Types partagés si monorepo
🎓 Exemple Complet : CreateClub Flow
1. Backend DTO
// backend/src/club-management/presentation/dtos/create-club.dto.ts export class CreateClubDto { @IsString() @MinLength(3) @MaxLength(100) readonly name: string; @IsString() @IsOptional() @MaxLength(500) readonly description?: string; }
2. Frontend Schema (Zod)
// frontend/src/features/club-management/schemas/club.schema.ts export const createClubSchema = z.object({ name: z.string().min(3).max(100), description: z.string().max(500).optional(), });
3. Server Action
// frontend/src/features/club-management/actions/create-club.action.ts export async function createClubAction(input: CreateClubInput) { const validated = createClubSchema.parse(input); // Frontend validation const response = await clubsApi.create(validated); // Backend call revalidatePath('/dashboard/coach'); return { success: true, data: response }; }
4. Component Usage
// frontend/src/features/club-management/components/ClubCreationForm.tsx 'use client'; import { useTransition } from 'react'; import { createClubAction } from '../actions/create-club.action'; export function ClubCreationForm() { const [isPending, startTransition] = useTransition(); const handleSubmit = async (formData: FormData) => { startTransition(async () => { const result = await createClubAction({ name: formData.get('name') as string, description: formData.get('description') as string, }); if (result.success) { router.push(`/clubs/${result.data.id}`); } else { setError(result.error.message); } }); }; return <form action={handleSubmit}>...</form>; }
🚨 Erreurs Courantes à Éviter
-
❌ Types incohérents
- ✅ FAIRE : Générer types frontend depuis Swagger ou partager
- ❌ NE PAS FAIRE : Dupliquer manuellement sans synchronisation
-
❌ Validations divergentes
- ✅ FAIRE : Même règles backend (class-validator) et frontend (Zod)
- ❌ NE PAS FAIRE : Backend max=100, Frontend max=50
-
❌ Erreurs non standardisées
- ✅ FAIRE : Format uniforme
{ code, message, details } - ❌ NE PAS FAIRE : Formats différents selon l'endpoint
- ✅ FAIRE : Format uniforme
-
❌ Swagger obsolète
- ✅ FAIRE : Swagger généré automatiquement depuis les DTOs
- ❌ NE PAS FAIRE : Documentation manuelle non synchronisée
-
❌ Server Actions avec logique métier
- ✅ FAIRE : Server Actions = orchestration mince (appel API + cache)
- ❌ NE PAS FAIRE : Logique métier dans Server Actions
📚 Skills Complémentaires
Pour aller plus loin :
- server-actions : Patterns Server Actions Next.js détaillés
- ddd-bounded-context : Architecture backend DDD
- cqrs-command-query : Commands/Queries pour APIs
Rappel : La synchronisation parfaite Frontend ↔ Backend garantit une communication sans bugs et une expérience développeur optimale.

RomualdP
hoki
Download Skill Files
View Installation GuideDownload the complete skill directory including SKILL.md and all related files