NestJS + Prisma : La stack moderne pour le backend Node.js

Guide complet pour créer une API backend moderne avec NestJS et Prisma. Configuration, modèles, services, transactions et bonnes pratiques expliquées.

NestJS et Prisma pour créer une stack backend moderne

NestJS et Prisma forment une combinaison puissante pour le développement backend moderne. NestJS apporte une architecture modulaire et l'injection de dépendances, tandis que Prisma offre un ORM type-safe avec une expérience développeur exceptionnelle. Cette stack permet de construire des APIs robustes et maintenables.

Pourquoi cette combinaison

Prisma génère automatiquement un client TypeScript typé à partir du schéma de base de données. Combiné avec NestJS et son système de modules, le code devient auto-documenté et les erreurs de type sont détectées à la compilation.

Installation et configuration initiale

La mise en place de Prisma dans un projet NestJS nécessite quelques étapes de configuration. Le processus est standard et bien documenté.

bash
# terminal
# Création d'un nouveau projet NestJS
nest new my-backend-api
cd my-backend-api

# Installation de Prisma comme dépendance de développement
npm install prisma --save-dev

# Installation du client Prisma
npm install @prisma/client

# Initialisation de Prisma avec PostgreSQL
npx prisma init --datasource-provider postgresql

Cette commande crée un dossier prisma/ avec le fichier schema.prisma et un fichier .env pour les variables d'environnement.

src/prisma/prisma.service.tstypescript
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  // Logger dédié pour les opérations Prisma
  private readonly logger = new Logger(PrismaService.name);

  constructor() {
    // Configuration du client avec logging en développement
    super({
      log: process.env.NODE_ENV === 'development'
        ? ['query', 'info', 'warn', 'error']
        : ['error'],
    });
  }

  // Connexion automatique au démarrage du module
  async onModuleInit() {
    await this.$connect();
    this.logger.log('Connexion Prisma établie');
  }

  // Déconnexion propre à l'arrêt de l'application
  async onModuleDestroy() {
    await this.$disconnect();
    this.logger.log('Connexion Prisma fermée');
  }
}

Le service Prisma est maintenant prêt à être injecté dans les autres services de l'application.

Création du module Prisma global

Pour rendre le service Prisma disponible dans toute l'application sans l'importer explicitement dans chaque module, le décorateur @Global() est utilisé.

src/prisma/prisma.module.tstypescript
import { Global, Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

// Le décorateur @Global rend ce module disponible partout
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

L'import dans le module racine rend le service disponible automatiquement.

src/app.module.tstypescript
import { Module } from '@nestjs/common';
import { PrismaModule } from './prisma/prisma.module';
import { UsersModule } from './users/users.module';
import { PostsModule } from './posts/posts.module';

@Module({
  imports: [
    PrismaModule,   // Une seule déclaration suffit
    UsersModule,
    PostsModule,
  ],
})
export class AppModule {}

Désormais, n'importe quel service peut injecter PrismaService sans import supplémentaire.

Module global vs imports explicites

Un module global simplifie l'architecture mais rend les dépendances implicites. Pour les petites applications, c'est acceptable. Pour les projets de grande taille, des imports explicites peuvent améliorer la traçabilité des dépendances.

Définition du schéma Prisma

Le fichier schema.prisma définit les modèles de données, les relations et les options de base de données. Prisma utilise son propre langage de définition de schéma (PSL).

prisma/schema.prismaprisma
generator client {
  provider = "prisma-client-js"
  // Active les types pour les requêtes de filtrage avancées
  previewFeatures = ["fullTextSearch"]
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

// Modèle utilisateur avec toutes les relations
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  password  String
  name      String
  role      Role     @default(USER)
  createdAt DateTime @default(now()) @map("created_at")
  updatedAt DateTime @updatedAt @map("updated_at")

  // Relations one-to-many
  posts     Post[]
  comments  Comment[]
  profile   Profile?

  // Index pour les recherches fréquentes
  @@index([email])
  @@map("users")
}

// Enum pour les rôles utilisateur
enum Role {
  USER
  ADMIN
  MODERATOR
}

// Relation one-to-one avec User
model Profile {
  id       String  @id @default(cuid())
  bio      String?
  avatar   String?
  website  String?
  userId   String  @unique @map("user_id")

  user     User    @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@map("profiles")
}

// Articles avec relation many-to-one vers User
model Post {
  id          String    @id @default(cuid())
  title       String
  slug        String    @unique
  content     String
  excerpt     String?
  published   Boolean   @default(false)
  publishedAt DateTime? @map("published_at")
  authorId    String    @map("author_id")
  createdAt   DateTime  @default(now()) @map("created_at")
  updatedAt   DateTime  @updatedAt @map("updated_at")

  // Relations
  author      User      @relation(fields: [authorId], references: [id], onDelete: Cascade)
  comments    Comment[]
  categories  CategoriesOnPosts[]

  @@index([authorId])
  @@index([slug])
  @@map("posts")
}

// Commentaires avec double relation
model Comment {
  id        String   @id @default(cuid())
  content   String
  postId    String   @map("post_id")
  authorId  String   @map("author_id")
  createdAt DateTime @default(now()) @map("created_at")

  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)

  @@index([postId])
  @@map("comments")
}

// Catégories pour les articles
model Category {
  id    String @id @default(cuid())
  name  String @unique
  slug  String @unique

  posts CategoriesOnPosts[]

  @@map("categories")
}

// Table pivot pour la relation many-to-many
model CategoriesOnPosts {
  postId     String   @map("post_id")
  categoryId String   @map("category_id")
  assignedAt DateTime @default(now()) @map("assigned_at")

  post       Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  category   Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)

  @@id([postId, categoryId])
  @@map("categories_on_posts")
}

Après modification du schéma, la migration applique les changements à la base de données.

bash
# terminal
# Création d'une migration avec nom descriptif
npx prisma migrate dev --name init_schema

# Génération du client Prisma (automatique après migrate dev)
npx prisma generate

# Visualisation du schéma dans le navigateur
npx prisma studio

Implémentation du service Users avec Prisma

Le service Users illustre les opérations CRUD courantes avec Prisma. Le typage automatique garantit la cohérence entre le code et la base de données.

src/users/users.service.tstypescript
import {
  Injectable,
  NotFoundException,
  ConflictException,
  BadRequestException,
} from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User, Prisma } from '@prisma/client';
import * as bcrypt from 'bcrypt';

// Type pour les résultats sans mot de passe
type SafeUser = Omit<User, 'password'>;

@Injectable()
export class UsersService {
  constructor(private readonly prisma: PrismaService) {}

  // Sélection par défaut sans le mot de passe
  private readonly safeSelect: Prisma.UserSelect = {
    id: true,
    email: true,
    name: true,
    role: true,
    createdAt: true,
    updatedAt: true,
    profile: true,
  };

  async create(createUserDto: CreateUserDto): Promise<SafeUser> {
    // Vérification de l'unicité de l'email
    const existingUser = await this.prisma.user.findUnique({
      where: { email: createUserDto.email },
    });

    if (existingUser) {
      throw new ConflictException('Cet email est déjà utilisé');
    }

    // Hashage sécurisé du mot de passe
    const hashedPassword = await bcrypt.hash(createUserDto.password, 12);

    // Création avec profil optionnel
    return this.prisma.user.create({
      data: {
        email: createUserDto.email,
        password: hashedPassword,
        name: createUserDto.name,
        // Création imbriquée du profil si fourni
        profile: createUserDto.bio ? {
          create: {
            bio: createUserDto.bio,
          },
        } : undefined,
      },
      select: this.safeSelect,
    });
  }

  async findAll(params: {
    page?: number;
    limit?: number;
    search?: string;
  }): Promise<{ data: SafeUser[]; total: number; pages: number }> {
    const { page = 1, limit = 10, search } = params;
    const skip = (page - 1) * limit;

    // Condition de recherche optionnelle
    const where: Prisma.UserWhereInput = search
      ? {
          OR: [
            { email: { contains: search, mode: 'insensitive' } },
            { name: { contains: search, mode: 'insensitive' } },
          ],
        }
      : {};

    // Exécution parallèle pour les performances
    const [data, total] = await this.prisma.$transaction([
      this.prisma.user.findMany({
        where,
        skip,
        take: limit,
        orderBy: { createdAt: 'desc' },
        select: this.safeSelect,
      }),
      this.prisma.user.count({ where }),
    ]);

    return {
      data,
      total,
      pages: Math.ceil(total / limit),
    };
  }

  async findOne(id: string): Promise<SafeUser> {
    const user = await this.prisma.user.findUnique({
      where: { id },
      select: {
        ...this.safeSelect,
        // Inclusion des posts récents
        posts: {
          take: 5,
          orderBy: { createdAt: 'desc' },
          select: {
            id: true,
            title: true,
            slug: true,
            published: true,
          },
        },
      },
    });

    if (!user) {
      throw new NotFoundException(`Utilisateur avec l'ID ${id} introuvable`);
    }

    return user;
  }

  async update(id: string, updateUserDto: UpdateUserDto): Promise<SafeUser> {
    // Vérification de l'existence
    await this.findOne(id);

    // Mise à jour avec gestion du profil imbriqué
    return this.prisma.user.update({
      where: { id },
      data: {
        name: updateUserDto.name,
        // Mise à jour ou création du profil
        profile: updateUserDto.bio ? {
          upsert: {
            create: { bio: updateUserDto.bio },
            update: { bio: updateUserDto.bio },
          },
        } : undefined,
      },
      select: this.safeSelect,
    });
  }

  async remove(id: string): Promise<void> {
    await this.findOne(id);
    // La suppression cascade vers le profil et les posts
    await this.prisma.user.delete({ where: { id } });
  }
}

Le typage Prisma garantit que toutes les propriétés utilisées existent dans le schéma.

Prêt à réussir tes entretiens Node.js / NestJS ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Gestion des relations avec Prisma

Prisma simplifie la gestion des relations complexes. Les requêtes imbriquées permettent de charger les données associées en une seule requête.

src/posts/posts.service.tstypescript
import { Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePostDto } from './dto/create-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Prisma } from '@prisma/client';

@Injectable()
export class PostsService {
  constructor(private readonly prisma: PrismaService) {}

  async create(authorId: string, createPostDto: CreatePostDto) {
    // Génération automatique du slug à partir du titre
    const slug = this.generateSlug(createPostDto.title);

    return this.prisma.post.create({
      data: {
        title: createPostDto.title,
        slug,
        content: createPostDto.content,
        excerpt: createPostDto.excerpt,
        // Connexion à l'auteur existant
        author: {
          connect: { id: authorId },
        },
        // Connexion aux catégories existantes
        categories: createPostDto.categoryIds ? {
          create: createPostDto.categoryIds.map(categoryId => ({
            category: { connect: { id: categoryId } },
          })),
        } : undefined,
      },
      include: {
        author: {
          select: { id: true, name: true },
        },
        categories: {
          include: {
            category: true,
          },
        },
      },
    });
  }

  async findAllPublished(params: {
    page?: number;
    limit?: number;
    categorySlug?: string;
  }) {
    const { page = 1, limit = 10, categorySlug } = params;
    const skip = (page - 1) * limit;

    // Filtre conditionnel par catégorie
    const where: Prisma.PostWhereInput = {
      published: true,
      ...(categorySlug && {
        categories: {
          some: {
            category: { slug: categorySlug },
          },
        },
      }),
    };

    const [posts, total] = await this.prisma.$transaction([
      this.prisma.post.findMany({
        where,
        skip,
        take: limit,
        orderBy: { publishedAt: 'desc' },
        include: {
          author: {
            select: { id: true, name: true },
          },
          categories: {
            include: {
              category: { select: { name: true, slug: true } },
            },
          },
          _count: {
            select: { comments: true },
          },
        },
      }),
      this.prisma.post.count({ where }),
    ]);

    return { posts, total, pages: Math.ceil(total / limit) };
  }

  async findBySlug(slug: string) {
    const post = await this.prisma.post.findUnique({
      where: { slug },
      include: {
        author: {
          select: { id: true, name: true, profile: true },
        },
        categories: {
          include: {
            category: true,
          },
        },
        comments: {
          orderBy: { createdAt: 'desc' },
          take: 20,
          include: {
            author: {
              select: { id: true, name: true },
            },
          },
        },
      },
    });

    if (!post) {
      throw new NotFoundException(`Article "${slug}" introuvable`);
    }

    return post;
  }

  async publish(id: string, authorId: string) {
    // Vérification que l'auteur est bien le propriétaire
    const post = await this.prisma.post.findUnique({
      where: { id },
      select: { authorId: true },
    });

    if (!post) {
      throw new NotFoundException(`Article avec l'ID ${id} introuvable`);
    }

    if (post.authorId !== authorId) {
      throw new ForbiddenException('Publication non autorisée');
    }

    return this.prisma.post.update({
      where: { id },
      data: {
        published: true,
        publishedAt: new Date(),
      },
    });
  }

  private generateSlug(title: string): string {
    return title
      .toLowerCase()
      .normalize('NFD')
      .replace(/[\u0300-\u036f]/g, '')
      .replace(/[^a-z0-9]+/g, '-')
      .replace(/(^-|-$)/g, '');
  }
}

Les includes permettent de récupérer les relations en profondeur tout en contrôlant les champs retournés.

Transactions et opérations atomiques

Prisma propose plusieurs méthodes pour garantir l'atomicité des opérations. Les transactions interactives offrent le plus de flexibilité.

src/orders/orders.service.tstypescript
import { Injectable, BadRequestException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreateOrderDto } from './dto/create-order.dto';

@Injectable()
export class OrdersService {
  constructor(private readonly prisma: PrismaService) {}

  async createOrder(userId: string, createOrderDto: CreateOrderDto) {
    // Transaction interactive pour garantir l'atomicité
    return this.prisma.$transaction(async (tx) => {
      // 1. Vérification du stock pour chaque article
      const items = await Promise.all(
        createOrderDto.items.map(async (item) => {
          const product = await tx.product.findUnique({
            where: { id: item.productId },
          });

          if (!product) {
            throw new BadRequestException(
              `Produit ${item.productId} introuvable`
            );
          }

          if (product.stock < item.quantity) {
            throw new BadRequestException(
              `Stock insuffisant pour ${product.name}`
            );
          }

          return { product, quantity: item.quantity };
        })
      );

      // 2. Calcul du total
      const total = items.reduce(
        (sum, { product, quantity }) => sum + product.price * quantity,
        0
      );

      // 3. Création de la commande
      const order = await tx.order.create({
        data: {
          userId,
          total,
          status: 'PENDING',
          items: {
            create: items.map(({ product, quantity }) => ({
              productId: product.id,
              quantity,
              price: product.price,
            })),
          },
        },
        include: {
          items: {
            include: { product: true },
          },
        },
      });

      // 4. Mise à jour du stock
      await Promise.all(
        items.map(({ product, quantity }) =>
          tx.product.update({
            where: { id: product.id },
            data: { stock: { decrement: quantity } },
          })
        )
      );

      return order;
    });
  }

  async cancelOrder(orderId: string, userId: string) {
    return this.prisma.$transaction(async (tx) => {
      // Récupération de la commande avec ses articles
      const order = await tx.order.findUnique({
        where: { id: orderId },
        include: { items: true },
      });

      if (!order || order.userId !== userId) {
        throw new BadRequestException('Commande introuvable');
      }

      if (order.status !== 'PENDING') {
        throw new BadRequestException(
          'Seules les commandes en attente peuvent être annulées'
        );
      }

      // Restauration du stock
      await Promise.all(
        order.items.map((item) =>
          tx.product.update({
            where: { id: item.productId },
            data: { stock: { increment: item.quantity } },
          })
        )
      );

      // Mise à jour du statut
      return tx.order.update({
        where: { id: orderId },
        data: { status: 'CANCELLED' },
      });
    });
  }
}

Les transactions garantissent que toutes les opérations réussissent ensemble ou échouent ensemble.

Timeout des transactions

Par défaut, les transactions Prisma ont un timeout de 5 secondes. Pour les opérations longues, ce paramètre peut être ajusté avec $transaction([...], { timeout: 10000 }).

Middleware Prisma pour l'audit

Les middlewares Prisma permettent d'intercepter les requêtes pour ajouter des comportements transversaux comme l'audit ou le soft delete.

src/prisma/prisma.service.ts (version avec middleware)typescript
import { Injectable, OnModuleInit, OnModuleDestroy, Logger } from '@nestjs/common';
import { PrismaClient, Prisma } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  private readonly logger = new Logger(PrismaService.name);

  constructor() {
    super({
      log: [
        { level: 'query', emit: 'event' },
        { level: 'error', emit: 'stdout' },
      ],
    });

    // Middleware pour l'audit des modifications
    this.$use(async (params: Prisma.MiddlewareParams, next) => {
      const start = Date.now();

      // Exécution de la requête
      const result = await next(params);

      const duration = Date.now() - start;

      // Log des requêtes lentes (> 100ms)
      if (duration > 100) {
        this.logger.warn(
          `Requête lente: ${params.model}.${params.action} - ${duration}ms`
        );
      }

      // Audit des opérations d'écriture
      if (['create', 'update', 'delete'].includes(params.action)) {
        this.logger.log(
          `Audit: ${params.action} on ${params.model} - ${duration}ms`
        );
      }

      return result;
    });

    // Middleware pour le soft delete automatique
    this.$use(async (params, next) => {
      // Transformation des delete en update pour certains modèles
      if (params.model === 'User' && params.action === 'delete') {
        params.action = 'update';
        params.args['data'] = { deletedAt: new Date() };
      }

      // Exclusion automatique des enregistrements supprimés
      if (params.model === 'User' && params.action === 'findMany') {
        if (!params.args) params.args = {};
        if (!params.args.where) params.args.where = {};
        params.args.where.deletedAt = null;
      }

      return next(params);
    });
  }

  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}

Les middlewares s'exécutent dans l'ordre d'enregistrement et peuvent modifier les paramètres de requête.

Optimisation des performances avec Prisma

Plusieurs techniques permettent d'optimiser les performances des requêtes Prisma dans une application NestJS.

src/common/prisma-extensions.tstypescript
import { Prisma, PrismaClient } from '@prisma/client';

// Extension pour la pagination standardisée
export const paginationExtension = Prisma.defineExtension({
  model: {
    $allModels: {
      async paginate<T, A>(
        this: T,
        args: Prisma.Exact<A, Prisma.Args<T, 'findMany'>> & {
          page?: number;
          limit?: number;
        }
      ): Promise<{
        data: Prisma.Result<T, A, 'findMany'>;
        meta: { page: number; limit: number; total: number; pages: number };
      }> {
        const { page = 1, limit = 10, ...rest } = args as any;
        const skip = (page - 1) * limit;

        const context = Prisma.getExtensionContext(this);
        const [data, total] = await Promise.all([
          (context as any).findMany({ ...rest, skip, take: limit }),
          (context as any).count({ where: (rest as any).where }),
        ]);

        return {
          data,
          meta: {
            page,
            limit,
            total,
            pages: Math.ceil(total / limit),
          },
        };
      },
    },
  },
});

// Utilisation dans le service
// const result = await this.prisma.$extends(paginationExtension)
//   .user.paginate({ page: 2, limit: 20, where: { role: 'USER' } });

Pour les requêtes fréquentes, la mise en cache améliore significativement les temps de réponse.

src/posts/posts.service.ts (avec cache)typescript
import { Injectable, Inject } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { PrismaService } from '../prisma/prisma.service';

@Injectable()
export class PostsService {
  constructor(
    private readonly prisma: PrismaService,
    @Inject(CACHE_MANAGER) private cacheManager: Cache,
  ) {}

  async findPopularPosts() {
    const cacheKey = 'posts:popular';

    // Tentative de récupération depuis le cache
    const cached = await this.cacheManager.get(cacheKey);
    if (cached) {
      return cached;
    }

    // Requête en base de données
    const posts = await this.prisma.post.findMany({
      where: { published: true },
      orderBy: { comments: { _count: 'desc' } },
      take: 10,
      include: {
        author: { select: { name: true } },
        _count: { select: { comments: true } },
      },
    });

    // Mise en cache pour 5 minutes
    await this.cacheManager.set(cacheKey, posts, 300000);

    return posts;
  }

  async invalidateCache(postId: string) {
    // Invalidation sélective du cache
    await this.cacheManager.del('posts:popular');
    await this.cacheManager.del(`post:${postId}`);
  }
}

Tests avec Prisma et NestJS

Les tests nécessitent une stratégie de base de données isolée. L'utilisation d'une base de données de test dédiée garantit la reproductibilité.

test/helpers/prisma-test.helper.tstypescript
import { PrismaClient } from '@prisma/client';
import { execSync } from 'child_process';

const prisma = new PrismaClient();

export async function setupTestDatabase() {
  // Utilisation d'une base de données de test
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;

  // Application des migrations
  execSync('npx prisma migrate deploy', {
    env: { ...process.env, DATABASE_URL: process.env.TEST_DATABASE_URL },
  });
}

export async function cleanupTestDatabase() {
  // Suppression de toutes les données dans l'ordre des dépendances
  const tablenames = await prisma.$queryRaw<Array<{ tablename: string }>>`
    SELECT tablename FROM pg_tables WHERE schemaname='public'
  `;

  for (const { tablename } of tablenames) {
    if (tablename !== '_prisma_migrations') {
      await prisma.$executeRawUnsafe(
        `TRUNCATE TABLE "public"."${tablename}" CASCADE;`
      );
    }
  }
}

export async function disconnectTestDatabase() {
  await prisma.$disconnect();
}

Les tests d'intégration utilisent ces helpers pour un environnement propre.

test/users.e2e-spec.tstypescript
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
import { PrismaService } from '../src/prisma/prisma.service';
import { cleanupTestDatabase, setupTestDatabase } from './helpers/prisma-test.helper';

describe('UsersController (e2e)', () => {
  let app: INestApplication;
  let prisma: PrismaService;

  beforeAll(async () => {
    await setupTestDatabase();

    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(new ValidationPipe({ whitelist: true }));

    prisma = app.get(PrismaService);
    await app.init();
  });

  beforeEach(async () => {
    await cleanupTestDatabase();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('POST /users', () => {
    it('should create a new user', async () => {
      const createUserDto = {
        email: 'test@example.com',
        password: 'Password123!',
        name: 'Test User',
      };

      const response = await request(app.getHttpServer())
        .post('/users')
        .send(createUserDto)
        .expect(201);

      expect(response.body).toMatchObject({
        email: createUserDto.email,
        name: createUserDto.name,
      });
      expect(response.body.password).toBeUndefined();
    });

    it('should reject duplicate email', async () => {
      const createUserDto = {
        email: 'duplicate@example.com',
        password: 'Password123!',
        name: 'First User',
      };

      await request(app.getHttpServer())
        .post('/users')
        .send(createUserDto)
        .expect(201);

      await request(app.getHttpServer())
        .post('/users')
        .send({ ...createUserDto, name: 'Second User' })
        .expect(409);
    });
  });
});

Conclusion

NestJS et Prisma forment une stack backend moderne et productive. Le typage automatique, les migrations déclaratives et l'intégration native avec NestJS permettent de développer des APIs robustes rapidement.

Checklist pour une intégration NestJS + Prisma réussie

  • ✅ Module Prisma global pour une injection simplifiée
  • ✅ Schéma Prisma avec relations et index optimisés
  • ✅ Services typés avec sélection explicite des champs
  • ✅ Transactions pour les opérations atomiques
  • ✅ Middlewares pour l'audit et le soft delete
  • ✅ Cache pour les requêtes fréquentes
  • ✅ Tests d'intégration avec base de données isolée
  • ✅ Pagination standardisée via extensions

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Cette combinaison tire parti des forces de chaque outil : l'architecture modulaire de NestJS pour la structure et Prisma pour la couche données type-safe. Le résultat est un code maintenable, testable et performant, adapté aux applications d'entreprise.

Tags

#nestjs
#prisma
#nodejs
#typescript
#backend

Partager

Articles similaires