NestJS + Prisma: el stack backend moderno para Node.js

Guía completa para construir una API backend moderna con NestJS y Prisma. Configuración, modelos, servicios, transacciones y buenas prácticas explicadas.

NestJS y Prisma para construir un stack backend moderno

NestJS y Prisma forman una combinación potente para el desarrollo backend moderno. NestJS aporta una arquitectura modular y la inyección de dependencias, mientras que Prisma ofrece un ORM tipado con una experiencia de desarrollo excepcional. Este stack permite construir APIs robustas y mantenibles.

Por qué esta combinación

Prisma genera automáticamente un cliente TypeScript tipado a partir del esquema de la base de datos. Combinado con NestJS y su sistema de módulos, el código se autodocumenta y los errores de tipo se detectan en tiempo de compilación.

Configuración inicial del proyecto

Integrar Prisma en un proyecto NestJS requiere algunos pasos de configuración. El proceso es estándar y está bien documentado.

bash
# terminal
# Create a new NestJS project
nest new my-backend-api
cd my-backend-api

# Install Prisma as a dev dependency
npm install prisma --save-dev

# Install the Prisma client
npm install @prisma/client

# Initialize Prisma with PostgreSQL
npx prisma init --datasource-provider postgresql

Este comando crea una carpeta prisma/ con el archivo schema.prisma y un archivo .env para las variables de entorno.

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 {
  // Dedicated logger for Prisma operations
  private readonly logger = new Logger(PrismaService.name);

  constructor() {
    // Configure client with logging in development
    super({
      log: process.env.NODE_ENV === 'development'
        ? ['query', 'info', 'warn', 'error']
        : ['error'],
    });
  }

  // Automatic connection on module startup
  async onModuleInit() {
    await this.$connect();
    this.logger.log('Prisma connection established');
  }

  // Clean disconnection on application shutdown
  async onModuleDestroy() {
    await this.$disconnect();
    this.logger.log('Prisma connection closed');
  }
}

El servicio Prisma ya está listo para ser inyectado en el resto de servicios de la aplicación.

Crear el módulo Prisma global

Para que el servicio Prisma esté disponible en toda la aplicación sin importarlo explícitamente en cada módulo, se utiliza el decorador @Global().

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

// The @Global decorator makes this module available everywhere
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

La importación en el módulo raíz hace que el servicio quede disponible automáticamente.

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,   // Single declaration is sufficient
    UsersModule,
    PostsModule,
  ],
})
export class AppModule {}

De este modo, cualquier servicio puede inyectar PrismaService sin importaciones adicionales.

Módulo global vs importaciones explícitas

Un módulo global simplifica la arquitectura, pero vuelve implícitas las dependencias. Para aplicaciones pequeñas resulta aceptable. En proyectos grandes, las importaciones explícitas mejoran la trazabilidad de las dependencias.

Definir el esquema Prisma

El archivo schema.prisma define los modelos de datos, las relaciones y las opciones de la base de datos. Prisma utiliza su propio lenguaje de definición de esquema (PSL).

prisma/schema.prismaprisma
generator client {
  provider = "prisma-client-js"
  // Enable types for advanced filtering queries
  previewFeatures = ["fullTextSearch"]
}

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

// User model with all 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")

  // One-to-many relations
  posts     Post[]
  comments  Comment[]
  profile   Profile?

  // Index for frequent searches
  @@index([email])
  @@map("users")
}

// Enum for user roles
enum Role {
  USER
  ADMIN
  MODERATOR
}

// One-to-one relation with 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")
}

// Posts with many-to-one relation to 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")
}

// Comments with dual 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")
}

// Categories for posts
model Category {
  id    String @id @default(cuid())
  name  String @unique
  slug  String @unique

  posts CategoriesOnPosts[]

  @@map("categories")
}

// Pivot table for many-to-many relation
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")
}

Una vez modificado el esquema, las migraciones aplican los cambios a la base de datos.

bash
# terminal
# Create a migration with descriptive name
npx prisma migrate dev --name init_schema

# Generate Prisma client (automatic after migrate dev)
npx prisma generate

# View schema in browser
npx prisma studio

Implementar el servicio de usuarios con Prisma

El servicio Users ilustra las operaciones CRUD habituales con Prisma. El tipado automático garantiza la coherencia entre el código y la base de datos.

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 for results without password
type SafeUser = Omit<User, 'password'>;

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

  // Default selection without password
  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> {
    // Check email uniqueness
    const existingUser = await this.prisma.user.findUnique({
      where: { email: createUserDto.email },
    });

    if (existingUser) {
      throw new ConflictException('This email is already in use');
    }

    // Secure password hashing
    const hashedPassword = await bcrypt.hash(createUserDto.password, 12);

    // Creation with optional profile
    return this.prisma.user.create({
      data: {
        email: createUserDto.email,
        password: hashedPassword,
        name: createUserDto.name,
        // Nested profile creation if provided
        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;

    // Optional search condition
    const where: Prisma.UserWhereInput = search
      ? {
          OR: [
            { email: { contains: search, mode: 'insensitive' } },
            { name: { contains: search, mode: 'insensitive' } },
          ],
        }
      : {};

    // Parallel execution for performance
    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,
        // Include recent posts
        posts: {
          take: 5,
          orderBy: { createdAt: 'desc' },
          select: {
            id: true,
            title: true,
            slug: true,
            published: true,
          },
        },
      },
    });

    if (!user) {
      throw new NotFoundException(`User with ID ${id} not found`);
    }

    return user;
  }

  async update(id: string, updateUserDto: UpdateUserDto): Promise<SafeUser> {
    // Check existence
    await this.findOne(id);

    // Update with nested profile handling
    return this.prisma.user.update({
      where: { id },
      data: {
        name: updateUserDto.name,
        // Update or create profile
        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);
    // Deletion cascades to profile and posts
    await this.prisma.user.delete({ where: { id } });
  }
}

El tipado de Prisma garantiza que todas las propiedades utilizadas existen en el esquema.

¿Listo para aprobar tus entrevistas de Node.js / NestJS?

Practica con nuestros simuladores interactivos, flashcards y tests técnicos.

Gestionar las relaciones con Prisma

Prisma simplifica el manejo de relaciones complejas. Las consultas anidadas permiten cargar los datos relacionados en una sola petición.

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) {
    // Automatic slug generation from title
    const slug = this.generateSlug(createPostDto.title);

    return this.prisma.post.create({
      data: {
        title: createPostDto.title,
        slug,
        content: createPostDto.content,
        excerpt: createPostDto.excerpt,
        // Connect to existing author
        author: {
          connect: { id: authorId },
        },
        // Connect to existing categories
        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;

    // Conditional filter by category
    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(`Post "${slug}" not found`);
    }

    return post;
  }

  async publish(id: string, authorId: string) {
    // Verify author is the owner
    const post = await this.prisma.post.findUnique({
      where: { id },
      select: { authorId: true },
    });

    if (!post) {
      throw new NotFoundException(`Post with ID ${id} not found`);
    }

    if (post.authorId !== authorId) {
      throw new ForbiddenException('Publication not authorized');
    }

    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, '');
  }
}

Los include permiten obtener relaciones profundas controlando qué campos se devuelven.

Transacciones y operaciones atómicas

Prisma ofrece varios métodos para garantizar la atomicidad de las operaciones. Las transacciones interactivas aportan la mayor flexibilidad.

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) {
    // Interactive transaction to ensure atomicity
    return this.prisma.$transaction(async (tx) => {
      // 1. Verify stock for each item
      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(
              `Product ${item.productId} not found`
            );
          }

          if (product.stock < item.quantity) {
            throw new BadRequestException(
              `Insufficient stock for ${product.name}`
            );
          }

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

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

      // 3. Create order
      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. Update 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) => {
      // Get order with items
      const order = await tx.order.findUnique({
        where: { id: orderId },
        include: { items: true },
      });

      if (!order || order.userId !== userId) {
        throw new BadRequestException('Order not found');
      }

      if (order.status !== 'PENDING') {
        throw new BadRequestException(
          'Only pending orders can be cancelled'
        );
      }

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

      // Update status
      return tx.order.update({
        where: { id: orderId },
        data: { status: 'CANCELLED' },
      });
    });
  }
}

Las transacciones aseguran que todas las operaciones se completen juntas o fallen juntas.

Timeout de las transacciones

Por defecto, las transacciones de Prisma tienen un timeout de 5 segundos. Para operaciones largas, se puede ajustar con $transaction([...], { timeout: 10000 }).

Middlewares de Prisma para auditoría

Los middlewares de Prisma permiten interceptar las consultas para añadir comportamientos transversales como la auditoría o el soft delete.

src/prisma/prisma.service.ts (version with 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 for modification auditing
    this.$use(async (params: Prisma.MiddlewareParams, next) => {
      const start = Date.now();

      // Execute query
      const result = await next(params);

      const duration = Date.now() - start;

      // Log slow queries (> 100ms)
      if (duration > 100) {
        this.logger.warn(
          `Slow query: ${params.model}.${params.action} - ${duration}ms`
        );
      }

      // Audit write operations
      if (['create', 'update', 'delete'].includes(params.action)) {
        this.logger.log(
          `Audit: ${params.action} on ${params.model} - ${duration}ms`
        );
      }

      return result;
    });

    // Middleware for automatic soft delete
    this.$use(async (params, next) => {
      // Transform delete to update for certain models
      if (params.model === 'User' && params.action === 'delete') {
        params.action = 'update';
        params.args['data'] = { deletedAt: new Date() };
      }

      // Automatic exclusion of deleted records
      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();
  }
}

Los middlewares se ejecutan en el orden de su registro y pueden modificar los parámetros de la consulta.

Optimización del rendimiento con Prisma

Varias técnicas optimizan el rendimiento de las consultas Prisma en una aplicación NestJS.

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

// Extension for standardized pagination
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),
          },
        };
      },
    },
  },
});

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

Para las consultas frecuentes, la caché mejora notablemente los tiempos de respuesta.

src/posts/posts.service.ts (with 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';

    // Try to get from cache
    const cached = await this.cacheManager.get(cacheKey);
    if (cached) {
      return cached;
    }

    // Database query
    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 } },
      },
    });

    // Cache for 5 minutes
    await this.cacheManager.set(cacheKey, posts, 300000);

    return posts;
  }

  async invalidateCache(postId: string) {
    // Selective cache invalidation
    await this.cacheManager.del('posts:popular');
    await this.cacheManager.del(`post:${postId}`);
  }
}

Pruebas con Prisma y NestJS

Las pruebas requieren una estrategia de base de datos aislada. Utilizar una base de datos dedicada a los tests garantiza la reproducibilidad.

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

const prisma = new PrismaClient();

export async function setupTestDatabase() {
  // Use a test database
  process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;

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

export async function cleanupTestDatabase() {
  // Delete all data in dependency order
  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();
}

Los tests de integración utilizan estos helpers para garantizar un entorno limpio.

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);
    });
  });
});

Conclusión

NestJS y Prisma forman un stack backend moderno y productivo. El tipado automático, las migraciones declarativas y la integración nativa con NestJS permiten desarrollar APIs robustas con rapidez.

Checklist para una integración NestJS + Prisma exitosa

  • ✅ Módulo Prisma global para una inyección simplificada
  • ✅ Esquema Prisma con relaciones e índices optimizados
  • ✅ Servicios tipados con selección explícita de campos
  • ✅ Transacciones para operaciones atómicas
  • ✅ Middlewares para auditoría y soft delete
  • ✅ Caché para las consultas frecuentes
  • ✅ Tests de integración con base de datos aislada
  • ✅ Paginación estandarizada mediante extensiones

¡Empieza a practicar!

Pon a prueba tu conocimiento con nuestros simuladores de entrevista y tests técnicos.

Esta combinación aprovecha los puntos fuertes de cada herramienta: la arquitectura modular de NestJS para la estructura y Prisma para una capa de datos tipada. El resultado es código mantenible, testeable y eficiente, adecuado para aplicaciones empresariales.

Etiquetas

#nestjs
#prisma
#nodejs
#typescript
#backend

Compartir

Artículos relacionados