Saltar al contenido principal

📘 Manual de Prácticas para el Desarrollo Backend con NestJS

Estas son las convenciones que vamos a utilizar para nuestro código en NestJS.

1. 🧱 Arquitectura del Proyecto

🎯 Objetivo

Estructurar el proyecto para maximizar escalabilidad, mantenibilidad y claridad. Una arquitectura clara previene la deuda técnica, facilita la colaboración entre equipos y permite iterar de forma segura.

📁 Estructura recomendada

bash
CopiarEditar
src/
├── main.ts # Punto de entrada principal
├── app.module.ts # Módulo raíz
├── config/ # Configuración del entorno y validación
│ ├── configuration.ts
│ └── validation.schema.ts
├── modules/ # Cada feature funcional en su módulo
│ ├── users/
│ └── auth/
├── common/ # Código compartido y reutilizable
│ ├── interceptors/
│ ├── filters/
│ ├── decorators/
│ ├── exceptions/
│ └── utils/
├── database/ # Config ORM, seeds, migraciones
├── middleware/ # Middleware personalizados
├── guards/ # Guards globales o de módulo
├── pipes/ # Pipes reutilizables
├── tests/ # Pruebas unitarias y E2E
└── assets/ # Archivos estáticos, logos, etc.

¿Por qué esta estructura?

  • Mantiene la cohesión funcional agrupando por feature.
  • Favorece el desacoplamiento.
  • Facilita el testeo y la escalabilidad del sistema.

2. 📦 Organización de Módulos

🎯 Objetivo

Separar el dominio en contextos independientes para encapsular reglas, datos y controladores, evitando acoplamiento innecesario.

🧩 Principios

  • Un módulo debe contener solo lo necesario para su contexto (controller, service, dto, entity, repository).
  • Reutilizar lógica común vía common/, no entre módulos directos.
  • Exponer servicios o providers usando exports solo si es imprescindible.

Ejemplo

ts
CopiarEditar
@Module({
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService], // Si AuthModule necesita acceder a UsersService
})
export class UsersModule {}

¿Por qué?

  • Mejora la independencia funcional de cada módulo.
  • Permite la carga perezosa (lazy loading) y facilita microservicios.
  • Simplifica testing y mantenimiento.

3. 🧠 Principios SOLID en NestJS

🎯 Objetivo

Diseñar clases y servicios que sean extensibles, testeables y mantenibles.

Desglose aplicado:

  • S (Single Responsibility)

    Cada clase debe encargarse de una sola cosa. Un UserService no debería autenticar usuarios ni enviar correos.

  • O (Open/Closed)

    Usa herencia o composición para extender lógica sin modificar código existente.

  • L (Liskov Substitution)

    Las subclases deben ser intercambiables por sus clases padre sin afectar la funcionalidad.

    👉 Usa interfaces y principios contractuales.

  • I (Interface Segregation)

    Prefiere muchas interfaces pequeñas sobre una general.

    👉 IUserRepository, IAuthProvider, etc.

  • D (Dependency Inversion)

    Usa inyección de dependencias (@Injectable()) en lugar de instanciar clases directamente.

    👉 Facilita mocks en testing.


4. 🧭 Controladores

🎯 Objetivo

Exponer las rutas de la API de manera limpia, delegando toda la lógica de negocio a los servicios.

🧼 Buenas prácticas

  • No implementar lógica de negocio en los controladores.
  • Delegar al Service.
  • Validar entradas usando DTOs y ValidationPipe.
  • Usar guards, interceptores y decorators para manejo transversal.

Ejemplo avanzado

ts
CopiarEditar
@UseGuards(JwtAuthGuard)
@Roles('admin')
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Post()
@HttpCode(201)
async create(@Body() dto: CreateUserDto) {
return this.usersService.createUser(dto);
}

@Get(':id')
async findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.usersService.getById(id);
}
}

¿Por qué?

  • Permite controladores finos, fáciles de mantener y testear.
  • Hace explícito el contrato de entrada y salida.

5. ⚙️ Servicios

🎯 Objetivo

Implementar la lógica de negocio y orquestar la interacción entre entidades, proveedores y repositorios.

🔑 Buenas prácticas

  • Mantener los servicios puros: sin lógica HTTP ni dependencias del framework.
  • Dividir en servicios de dominio y de infraestructura si el proyecto escala.
  • Inyectar dependencias, no instanciarlas.

Ejemplo

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

async createUser(dto: CreateUserDto): Promise<User> {
const user = await this.prisma.user.create({ data: dto });
// lógica adicional como enviar evento o log
return user;
}

async getById(id: string): Promise<User> {
const user = await this.prisma.user.findUnique({ where: { id } });
if (!user) throw new NotFoundException('User not found');
return user;
}
}

¿Por qué?

  • Facilita testeo unitario (mock de PrismaService).
  • Aísla la lógica de negocio de controladores, filtros e infraestructura.

6. 📥 DTOs y Validaciones

🎯 Objetivo

Asegurar que los datos entrantes cumplan con el contrato esperado, mejorando la seguridad y consistencia de la aplicación.

🔍 ¿Qué es un DTO?

Un Data Transfer Object es una clase que define explícitamente los campos que se reciben en una operación. Junto con class-validator y class-transformer, permite validar automáticamente los datos antes de que lleguen al servicio.

🧼 Buenas prácticas

  • Usar @IsString, @IsEmail, @IsOptional, etc., de class-validator.
  • Usar @Transform de class-transformer para castear valores.
  • Validar automáticamente usando ValidationPipe.

Ejemplo completo

ts
CopiarEditar
import { IsEmail, IsString, Length } from 'class-validator';

export class CreateUserDto {
@IsEmail()
email: string;

@IsString()
@Length(8, 20)
password: string;

@IsString()
fullName: string;
}

Y en el controlador:

ts
CopiarEditar
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true }))
create(@Body() dto: CreateUserDto) {
return this.userService.createUser(dto);
}

⚠️ whitelist elimina propiedades no declaradas. forbidNonWhitelisted lanza error si el usuario las envía. Esto mitiga ataques por sobreinclusión de datos.


7. 🛑 Manejo de Errores

🎯 Objetivo

Manejar errores de forma estructurada, coherente y segura, evitando exponer detalles internos y facilitando el diagnóstico.

🧱 Buenas prácticas

  • Usar HttpException para lanzar errores con status codes.
  • Crear ExceptionFilter global para formatear respuestas.
  • Usar catchAsync en servicios asíncronos si no se usa try/catch explícito.
  • Nunca exponer stack traces ni errores SQL en producción.

Ejemplo básico

ts
CopiarEditar
if (!user) {
throw new HttpException('User not found', HttpStatus.NOT_FOUND);
}

Filtro global de errores

ts
CopiarEditar
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const status = exception instanceof HttpException
? exception.getStatus()
: HttpStatus.INTERNAL_SERVER_ERROR;

const message = exception instanceof HttpException
? exception.getResponse()
: 'Internal server error';

response.status(status).json({
statusCode: status,
message,
});
}
}

Y se registra globalmente en main.ts:

ts
CopiarEditar
app.useGlobalFilters(new AllExceptionsFilter());

⚠️ Esto evita mensajes genéricos como UnhandledPromiseRejectionWarning, además de proteger detalles sensibles en producción.


8. 🛡️ Middleware, Guards, Interceptors y Pipes

Ya vimos parte de esta sección anteriormente, pero la completamos aquí con ejemplos más robustos:

🔒 Middleware Avanzado

ts
CopiarEditar
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`[${req.method}] ${req.originalUrl} - ${res.statusCode} (${duration}ms)`);
});
next();
}
}


🔐 Guard con Roles

ts
CopiarEditar
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}

canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
const { user } = context.switchToHttp().getRequest();
return requiredRoles?.some(role => user.roles?.includes(role)) ?? true;
}
}


🎯 Interceptor de Tiempos

ts
CopiarEditar
@Injectable()
export class ResponseTimeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const start = Date.now();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - start;
console.log(`Request handled in ${duration}ms`);
})
);
}
}


🧪 Pipe de UUID

ts
CopiarEditar
@Injectable()
export class ParseUUIDPipe implements PipeTransform {
transform(value: string): string {
if (!validate(value)) {
throw new BadRequestException('Invalid UUID');
}
return value;
}
}

Los Pipes permiten centralizar transformaciones o validaciones, en vez de colocarlas manualmente en cada controlador.


9. 🗃️ Acceso a Datos (ORM)

🎯 Objetivo

Desacoplar la lógica de negocio de la lógica de persistencia y asegurar un contrato tipado y seguro con la base de datos.

🔧 Buenas prácticas

  • Usar Prisma o TypeORM como ORM.
  • Separar repository de service si crece la lógica.
  • Usar migraciones versionadas y automatizadas.
  • Nunca retornar directamente modelos del ORM a los controladores.

Ejemplo con Prisma

ts
CopiarEditar
@Injectable()
export class UsersRepository {
constructor(private prisma: PrismaService) {}

async findByEmail(email: string): Promise<User | null> {
return this.prisma.user.findUnique({ where: { email } });
}

async create(data: Prisma.UserCreateInput): Promise<User> {
return this.prisma.user.create({ data });
}
}


10. 🔐 Seguridad

🎯 Objetivo

Mitigar riesgos de seguridad como XSS, CSRF, inyección de código o manipulación de sesiones.

🛡️ Buenas prácticas

  • Autenticación segura con JWT o sesiones firmadas.
  • Validación de entrada estricta con DTOs.
  • Activar helmet para proteger headers comunes.
  • Configurar correctamente CORS (@nestjs/platform-express).
  • Implementar rate limiting (rate-limiter-flexible o express-rate-limit).
  • Evitar filtrado SQL/ORM expuesto por el cliente.
  • Nunca incluir stack traces en respuestas HTTP.

Ejemplo de configuración de seguridad

ts
CopiarEditar
import helmet from 'helmet';
import rateLimit from 'express-rate-limit';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(helmet());
app.enableCors({
origin: ['https://tudominio.com'],
methods: ['GET', 'POST'],
credentials: true,
});
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));
await app.listen(3000);
}


11. ⚙️ Configuración y Environments

🎯 Objetivo

Separar la configuración del código para que la aplicación pueda adaptarse fácilmente a diferentes entornos (desarrollo, testing, producción) sin cambios de lógica.

🧰 Herramientas recomendadas

  • @nestjs/config: Gestión centralizada de .env.
  • dotenv: Carga de variables.
  • joi: Validación de esquema.

Buenas prácticas

  • Usar ConfigModule como módulo global.
  • Validar todas las variables del entorno al arrancar la app.
  • Definir un esquema explícito que evite configuraciones mal formadas.
  • Evitar imprimir en consola valores sensibles como claves API o credenciales.

Ejemplo

ts
CopiarEditar
// config/configuration.ts
export default () => ({
database: {
url: process.env.DATABASE_URL,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '3600s',
},
});

ts
CopiarEditar
// config/validation.schema.ts
import * as Joi from 'joi';

export default Joi.object({
DATABASE_URL: Joi.string().uri().required(),
JWT_SECRET: Joi.string().required(),
JWT_EXPIRES_IN: Joi.string().default('3600s'),
});

ts
CopiarEditar
// app.module.ts
ConfigModule.forRoot({
isGlobal: true,
load: [configuration],
validationSchema,
});


12. 🧪 Testing

🎯 Objetivo

Asegurar que las funcionalidades estén correctamente implementadas, prevenir regresiones y habilitar refactors seguros.

🧪 Tipos de pruebas

  • Unitarias: pruebas aisladas de métodos y lógica pura.
  • Integración: validan la interacción entre servicios y bases de datos.
  • End to End (E2E): simulan un uso real desde el cliente hasta el backend.

🔧 Herramientas

  • Jest: framework de pruebas por defecto en NestJS.
  • Supertest: para pruebas E2E.
  • @nestjs/testing: utilidad para crear contextos de pruebas.

Ejemplo unit test

ts
CopiarEditar
describe('UsersService', () => {
let service: UsersService;
let prisma: PrismaService;

beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [UsersService, PrismaService],
}).compile();

service = module.get(UsersService);
prisma = module.get(PrismaService);
});

it('should return user by id', async () => {
prisma.user.findUnique = jest.fn().mockResolvedValue({ id: '123', email: 'a@a.com' });
const result = await service.getById('123');
expect(result.email).toBe('a@a.com');
});
});

Ejemplo E2E

ts
CopiarEditar
describe('Auth (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
app = module.createNestApplication();
await app.init();
});

it('login returns access token', () => {
return request(app.getHttpServer())
.post('/auth/login')
.send({ email: 'test@example.com', password: 'password' })
.expect(201)
.expect(res => expect(res.body).toHaveProperty('accessToken'));
});
});


13. 📈 Logs y Monitoreo

🎯 Objetivo

Obtener visibilidad del comportamiento de la aplicación, errores en producción y métricas para análisis de performance.

🛠️ Herramientas

  • Logs estructurados: pino, winston, bunyan.
  • APM: Datadog, New Relic, Elastic APM.
  • Error tracking: Sentry, Rollbar.

Buenas prácticas

  • No loggear contraseñas ni datos sensibles.
  • Definir niveles: info, warn, error, debug.
  • Loggear errores con traceId o requestId para trazabilidad.

Ejemplo: logger con pino

ts
CopiarEditar
import { LoggerService } from '@nestjs/common';
import pino from 'pino';

const logger = pino({ level: 'info' });

export class PinoLogger implements LoggerService {
log(message: string) {
logger.info(message);
}
error(message: string, trace: string) {
logger.error({ message, trace });
}
warn(message: string) {
logger.warn(message);
}
}


14. 🚀 Performance

🎯 Objetivo

Mejorar el tiempo de respuesta, reducir la carga del servidor y ofrecer una experiencia fluida a los usuarios.

Estrategias comunes

  • Usar CacheInterceptor o nestjs/cache-manager para respuestas costosas.
  • Paginación obligatoria en queries grandes.
  • Lazy loading de módulos (forRootAsync, useFactory).
  • Uso eficiente de recursos (pool de conexiones, workers).
  • Delegar operaciones pesadas a colas (Bull, RabbitMQ).

Ejemplo: cache en endpoint

ts
CopiarEditar
@UseInterceptors(CacheInterceptor)
@Controller('products')
export class ProductsController {
@Get()
getAll() {
return this.productService.findAll(); // Resultado cacheado por 5 min
}
}


15. 🧾 Documentación de API

🎯 Objetivo

Exponer la API de forma clara para consumidores externos, QA o desarrolladores frontend, permitiendo validación de contratos.

Herramientas

  • @nestjs/swagger: genera documentación OpenAPI (Swagger).
  • Swagger UI: interfaz para probar endpoints desde el navegador.

Buenas prácticas

  • Documentar DTOs, parámetros, status de retorno y errores.
  • Usar ApiTags, ApiOperation, ApiResponse.

Ejemplo

ts
CopiarEditar
@ApiTags('users')
@Controller('users')
export class UsersController {
@Post()
@ApiOperation({ summary: 'Crear usuario' })
@ApiResponse({ status: 201, description: 'Usuario creado' })
@ApiResponse({ status: 400, description: 'Datos inválidos' })
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}


16. 🔁 Versionado de API

🎯 Objetivo

Permitir cambios evolutivos sin romper integraciones existentes.

Estrategias

  • Versionar por URL: /api/v1, /api/v2
  • Separar módulos o rutas por versión
  • Desactivar versiones antiguas con políticas definidas

Ejemplo

ts
CopiarEditar
@Controller('v1/users')
export class V1UsersController {}

@Controller('v2/users')
export class V2UsersController {}

Evita cambios breaking y facilita evolución del sistema.


17. 📦 DevOps y Despliegue

🎯 Objetivo

Automatizar, estandarizar y garantizar despliegues consistentes, seguros y reproducibles.

Buenas prácticas

  • Usar Docker para entornos consistentes.
  • Separar variables sensibles en .env fuera del repo.
  • Integrar pipelines de CI/CD: GitHub Actions, GitLab CI, CircleCI.
  • Validar dependencias con npm audit, npm outdated.
  • Aplicar lint, test y cobertura en cada PR.

Dockerfile recomendado

Dockerfile
CopiarEditar
FROM node:18-alpine
WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

EXPOSE 3000
CMD ["node", "dist/main.js"]

Ejemplo CI (GitHub Actions)

yaml
CopiarEditar
name: Build and Test
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 18
- run: npm ci
- run: npm run test


✅ Conclusión

Este manual proporciona una base sólida para el desarrollo profesional con NestJS. Cada sección busca fomentar aplicaciones:

  • Robustas: que funcionen bien ante errores y escalen con seguridad.
  • Seguras: mitigando amenazas comunes.
  • Escalables: tanto en tamaño como en equipo.
  • Mantenibles: con código limpio y estructurado.

Se recomienda adaptar estas buenas prácticas a los estándares de tu organización y revisarlas periódicamente para incorporar mejoras tecnológicas.