Cuando construyes una API con NestJS, llega un punto donde el controller empieza a hacer demasiado: expone endpoints, valida datos y, además, manipula la información. Ahí es donde entran los servicios en NestJS, la capa que centraliza la lógica de negocio y aplica principios de arquitectura limpia como single responsibility y singleton.
Si vienes trabajando solo con controladores, este es el salto que vuelve tu código escalable, reutilizable y fácil de mantener.
¿Por qué separar el controller del servicio en NestJS?
El problema aparece cuando un mismo archivo expone endpoints y, al mismo tiempo, decide cómo guardar, buscar o validar la data. Eso rompe el principio de responsabilidad única de SOLID.
En NestJS, la división se ve así:
- El controller expone el endpoint, define el protocolo (GET, POST, DELETE) y valida la entrada con DTOs.
- El servicio maneja la lógica de negocio: insertar, actualizar, buscar, eliminar.
- El model define la estructura interna de la entidad, distinta del DTO.
¿Qué hace un servicio en NestJS? Es una clase decorada con @Injectable() que contiene la lógica de negocio y se inyecta en los controladores u otros servicios mediante el constructor.
Normalmente vas a tener una paridad: una entidad, un controller, un servicio. Si tu base de datos tiene 20 tablas, probablemente tendrás cerca de 20 servicios, aunque no siempre 20 controllers, porque algunas entidades son internas y no se exponen [05:30].
¿Cómo crear un servicio con el CLI de Nest?
Desde la terminal, el generador de Nest crea el archivo, el test y actualiza el módulo automáticamente:
bash
nest generate service users
Esto produce tres acciones:
- Crea
users.service.ts con el decorador @Injectable().
- Crea el archivo de pruebas
users.service.spec.ts.
- Actualiza
app.module.ts para registrar el servicio en providers.
Antes de migrar la lógica, conviene mover la interface del usuario a un archivo user.model.ts y exportarla. Así separas claramente el DTO (validación de entrada) del model (esquema de la data guardada) [07:40].
¿Cómo funciona la inyección de dependencias y el patrón singleton?
Una vez creado el servicio, el controller lo recibe en el constructor:
ts
constructor(private usersService: UsersService) {}
Esa línea hace dos cosas. Primero, declara usersService como propiedad privada del controller. Segundo, le indica a NestJS que debe inyectar una instancia de UsersService.
Y aquí viene lo interesante: gracias al decorador @Injectable(), NestJS aplica el patrón singleton. Es decir, no importa cuántos controladores inyecten el mismo servicio, siempre se crea una sola instancia. Si se replicara, tendrías múltiples arrays de usuarios en memoria, cada uno con data distinta. Con singleton, todos apuntan a la misma fuente.
¿Qué es el patrón singleton en NestJS? Es un patrón donde una clase tiene una única instancia compartida en toda la aplicación. NestJS lo aplica por defecto a todo provider decorado con @Injectable().
¿Cómo migrar la lógica del controller al servicio?
La idea es vaciar el controller hasta dejarlo solo con la exposición del endpoint. Cada método del controller llama al servicio y retorna su resultado.
En el ejemplo de la clase, el servicio termina con estos métodos públicos:
findAll() retorna todos los usuarios.
findOne(id) busca un usuario por ID y aplica la regla de negocio que prohíbe acceder al ID 1 con un error 403 [13:20].
create(payload) recibe el CreateUserDTO, autogenera el ID e inserta en memoria.
update(id, changes) recibe el UpdateUserDTO y actualiza solo los campos enviados.
delete(id) elimina el usuario por posición.
Y un método privado clave: getUserById(id). Este se encarga de retornar la posición del usuario en el array y lanza NotFoundException si no existe. Es privado porque solo se usa internamente para reutilizar lógica entre update, delete y findOne.
El resultado es que el controller queda así de limpio:
ts
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
¿Dónde van las excepciones, en el controller o en el servicio?
Van en el servicio. NotFoundException o el error 403 de autorización forman parte de la lógica de negocio: depende de si el dato existe o cumple ciertas reglas. Manejarlas en el servicio evita duplicar validaciones y mantiene el controller enfocado solo en exponer rutas [16:50].
¿Por qué reutilizar DTOs también en el servicio?
Aunque la validación ya ocurrió en el controller gracias a class-validator, tipar los parámetros del servicio con el mismo DTO mantiene consistencia y aprovecha el contrato de datos. Cuando la información llega al servicio, ya viene con integridad garantizada: el email es válido, el nombre es string, los campos opcionales están marcados con @IsOptional().
¿Cómo resolver el reto del UpdateUserDTO?
La solución del reto anterior consistió en clonar el CreateUserDTO, renombrarlo a UpdateUserDTO y agregar el decorador @IsOptional() a cada campo. Así, los campos siguen siendo string o email válido, pero ya no son obligatorios.
Luego, en el método update del controller, en lugar de tipar el body con la interfaz, se usa UpdateUserDTO. Eso elimina las validaciones manuales dentro del método, porque la capa del DTO se encarga.
Para probarlo: si envías un name numérico, la API responde con un error indicando que debe ser string. Si envías solo name sin email, la actualización funciona porque el email es opcional [03:10].
¿Qué ganas al aplicar este patrón en tus APIs?
La refactorización deja varios beneficios concretos:
- Eliminas duplicación de código (la búsqueda por ID se centraliza en un método privado).
- Tu controller queda enfocado solo en exponer endpoints.
- Puedes inyectar el mismo servicio en varios controladores sin replicar instancias.
- Las excepciones viven junto a la lógica que las dispara.
- El código se vuelve testeable, porque el servicio se puede probar de forma aislada.
Ahora te dejo el reto de la clase: inyecta el UsersService también en el AppController. Vas a comprobar que NestJS mantiene una sola instancia compartida entre ambos controladores. ¿Lo intentaste? Cuéntame en los comentarios cómo te fue con la inyección desde el segundo controller.