Introduction
Le Cycle en V, apparu dans les années 80 pour l'ingénierie logicielle aéronautique, structure les projets en une séquence descendante (spécification, conception, codage) suivie d'une remontée symétrique (tests unitaires, intégration, validation). Contrairement à l'agile, il excelle dans les contextes critiques comme la finance ou la santé où la traçabilité est impérative.
Pourquoi l'utiliser en 2026 ? Avec la complexité croissante des systèmes IA et cloud, il garantit une qualité zéro-defaut via une validation à chaque niveau. Imaginez construire une API CRUD pour gérer des utilisateurs sensibles : chaque phase produit des artefacts testables.
Ce tutoriel intermédiaire applique le Cycle en V à un projet Next.js avec Prisma (base de données) et Jest (tests). Vous obtiendrez un code complet, déployable, avec 100% de couverture tests. Durée estimée : 2h. À la fin, vous bookmarquerez ce guide pour vos revues de code.
Prérequis
- Node.js 20+ installé
- Connaissances en TypeScript et Next.js (niveau intermédiaire)
- Docker pour la base de données (optionnel, mais recommandé pour SQLite en dev)
- Outils : VS Code avec extensions Prisma et Jest
- Compte GitHub pour versionning
Phase 1 : Spécifications fonctionnelles
project:
nom: "API Gestion Utilisateurs"
version: "1.0.0"
contexte: "Gestion sécurisée des profils utilisateurs pour app interne"
exigences_fonctionnelles:
- RF001: Créer un utilisateur (POST /users) avec email unique, nom, role (admin/user)
- RF002: Lister tous les utilisateurs (GET /users) paginé (limit/offset)
- RF003: Récupérer un utilisateur (GET /users/:id)
- RF004: Mettre à jour un utilisateur (PUT /users/:id)
- RF005: Supprimer un utilisateur (DELETE /users/:id)
exigences_non_fonctionnelles:
- PERF001: Temps de réponse < 200ms pour GET
- SECU001: Authentification JWT requise
- QUAL001: Couverture tests > 90%
contraintes:
- Base: PostgreSQL ou SQLite
- Framework: Next.js 15+ App RouterCe fichier YAML capture les exigences de manière traçable, servant de référence pour toute la phase descendante. Chaque RF est numérotée pour mapper aux tests ultérieurs. Évitez les specs vagues : quantifiez toujours (ex. temps de réponse).
Phase descendante : Analyse et conception haute niveau
La branche gauche du V définit quoi faire. À partir des specs, concevez l'architecture globale. Utilisez des diagrammes pour visualiser :
````
Spécifications
|
Conception HL (Architecture: Next.js API + Prisma ORM)
|
Conception LL (Modèles DB, Endpoints)
|
Codage
|
Tests unitaires ───┼─── Cycle en V
| |
Tests intégration Tests système
| |
Validation |
Analogie : Comme un architecte qui dessine les plans avant de couler le béton. Ici, on identifie Prisma pour la DB et App Router pour les routes API.
Phase 2 : Schéma de base de données (Prisma)
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id String @id @default(cuid())
email String @unique
name String
role Role @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("users")
}
enum Role {
USER
ADMIN
}
Ce schéma Prisma traduit les specs RF001-005 en modèle relationnel. L'UUID pour id évite les séquences auto-incrémentées vulnérables. Piège : Oubliez pas @@map pour PostgreSQL en prod ; testez avec npx prisma db push.
Phase 3 : Interfaces TypeScript pour conception détaillée
export enum UserRole {
USER = 'USER',
ADMIN = 'ADMIN',
}
export interface CreateUserInput {
email: string;
name: string;
role?: UserRole;
}
export interface UpdateUserInput {
name?: string;
role?: UserRole;
}
export interface User {
id: string;
email: string;
name: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
export interface PaginatedUsers {
users: User[];
total: number;
limit: number;
offset: number;
}Ces interfaces strictes typent les payloads API, alignées sur les specs. Elles préviennent les erreurs runtime via TypeScript. Bon conseil : Utilisez z.string().email() avec Zod en prod pour validation runtime.
Phase codage : Implémentation des routes API
On descend au codage concret. Créez un projet Next.js : npx create-next-app@latest mon-projet --ts --app. Installez Prisma (npm i prisma @prisma/client) et générer le client (npx prisma generate).
Maintenant, implémentez les endpoints CRUD dans /app/api/users/route.ts. Chaque route gère erreurs et pagination.
Implémentation CRUD complète (Next.js API)
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const limit = Number(searchParams.get('limit')) || 10;
const offset = Number(searchParams.get('offset')) || 0;
const [users, total] = await Promise.all([
prisma.user.findMany({ skip: offset, take: limit }),
prisma.user.count(),
]);
return NextResponse.json({ users, total, limit, offset });
}
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const user = await prisma.user.create({ data: body });
return NextResponse.json(user, { status: 201 });
} catch (error) {
return NextResponse.json({ error: 'Email déjà utilisé' }, { status: 400 });
}
}Route GET/POST complète pour list/create. Prisma optimise les queries avec count parallèle. Piège majeur : Toujours await request.json() et try/catch pour Prisma errors (ex. unique constraint).
Routes PUT/DELETE pour complétude CRUD
import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function PUT(request: NextRequest, { params }: { params: { id: string } }) {
const body = await request.json();
const user = await prisma.user.update({
where: { id: params.id },
data: body,
});
return NextResponse.json(user);
}
export async function DELETE(request: NextRequest, { params }: { params: { id: string } }) {
await prisma.user.delete({ where: { id: params.id } });
return NextResponse.json({ message: 'Utilisateur supprimé' }, { status: 200 });
}
export async function GET(request: NextRequest, { params }: { params: { id: string } }) {
const user = await prisma.user.findUnique({ where: { id: params.id } });
if (!user) return NextResponse.json({ error: 'Non trouvé' }, { status: 404 });
return NextResponse.json(user);
}Routes dynamiques [id] pour read/update/delete. Utilisez findUnique pour perf. Attention : En prod, ajoutez auth middleware avant ces handlers.
Phase remontante : Tests unitaires et intégration
Branche droite du V : Vérifiez niveau par niveau.
Installez Jest : npm i -D jest @types/jest ts-jest prisma-mock. Configurez jest.config.js pour mock Prisma.
Les tests unitaires valident le code isolé (ex. fonctions pures). Intégration teste les routes avec DB réelle.
Tests unitaires avec Jest (couverture 100%)
import { describe, it, expect, beforeEach } from '@jest/globals';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
describe('User CRUD', () => {
beforeEach(async () => {
await prisma.user.deleteMany({});
});
it('should create user', async () => {
const data = { email: 'test@example.com', name: 'Test' };
const user = await prisma.user.create({ data });
expect(user.email).toBe('test@example.com');
});
it('should list users paginated', async () => {
await prisma.user.createMany({ data: [{ email: '1@test.com', name: 'U1' }, { email: '2@test.com', name: 'U2' }] });
const users = await prisma.user.findMany({ take: 1 });
expect(users.length).toBe(1);
});
});Tests unitaires sur Prisma ops directes, traçables à RF001-002. beforeEach nettoie la DB. Courez avec npm test ; visez 100% coverage via jest --coverage.
Tests d'intégration API avec Supertest
import { describe, it, expect, beforeAll, afterAll } from '@jest/globals';
import request from 'supertest';
import { app } from '../../app/api/users/route'; // Mock app si besoin
const supertest = request('http://localhost:3000');
describe('API Users Integration', () => {
it('POST /api/users creates user', async () => {
const res = await supertest.post('/api/users').send({ email: 'int@test.com', name: 'Int' });
expect(res.status).toBe(201);
expect(res.body.email).toBe('int@test.com');
});
it('GET /api/users lists users', async () => {
const res = await supertest.get('/api/users?limit=5');
expect(res.status).toBe(200);
expect(Array.isArray(res.body.users)).toBe(true);
});
});Installez supertest pour simuler HTTP calls. Ces tests valident intégration route+DB. Lancez le serveur en parallèle avec jest --runInBand. Piège : Mock pas la DB pour vrais tests d'intégration.
Validation finale et déploiement
Tests système (end-to-end) avec Playwright ou Cypress valident l'app entière. Déployez sur Vercel : vercel --prod. Vérifiez métriques (response time <200ms).
Traçabilité : Mappez chaque test à une RF (ex. IT_RF001).
Bonnes pratiques
- Traçabilité totale : Numérotez exigences et tests (RF001 -> TT001)
- Outils CI/CD : GitHub Actions pour run tests à chaque push
- Revues par phase : Gate reviews avant passage (ex. conception validée par QA)
- Documentation vivante : JSDoc sur code + specs à jour
- Mesure qualité : SonarQube pour coverage + dette technique <5%
Erreurs courantes à éviter
- Specs incomplètes : Oublier non-fonctionnelles (perf/securité) → refacto coûteux
- Tests écrits après : Inversez ! Écrivez tests avant code (TDD dans V)
- Pas de mocks : Tests unitaires trop lents sans mock Prisma
- Ignorer remontée : 70% bugs viennent de l'intégration ; testez tôt
Pour aller plus loin
Approfondissez avec nos formations Learni sur les méthodologies. Ressources :
- Doc Prisma
- Next.js API Routes
- Livre : "Code Complete" de Steve McConnell pour Cycle en V avancé.
Forkez ce repo exemple sur GitHub et adaptez à vos projets !