📘 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
exportssolo 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
UserServiceno 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., declass-validator. - Usar
@Transformdeclass-transformerpara 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
HttpExceptionpara lanzar errores con status codes. - Crear
ExceptionFilterglobal para formatear respuestas. - Usar
catchAsyncen servicios asíncronos si no se usatry/catchexplí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
repositorydeservicesi 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
helmetpara proteger headers comunes. - Configurar correctamente CORS (
@nestjs/platform-express). - Implementar rate limiting (
rate-limiter-flexibleoexpress-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
ConfigModulecomo 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
traceIdorequestIdpara 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
CacheInterceptoronestjs/cache-managerpara 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
.envfuera 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.