NestJS — Aplicando SOLID

Claves para implementar correctamente SOLID en nuestros proyectos NestJS

Anartz Mugika Ledo🤗
6 min readJun 22, 2023

Comenzamos con un nuevo artículo en el que vamos a ver con más detalle como aplicar SOLID en NestJS de una forma clara y concisa.

Si queréis contenido de calidad como este artículo, os invito a que me sigáis y también os apuntéis a mi newsletter semanal, con 1–3 publicaciones, que dependerá en mayor o menor medida en relación a si se publican más artículos o menos.

La notificación siempre será por un nuevo artículo, no por nada más

Contenido del artículo

  • Introducción
  • ¿Qué es SOLID?
  • SOLID, definiciones de cada punto.
  • Aplicando los principios SOLID en NestJS
  • Conclusión

Introducción

NestJS es un popular framework de Node.js para construir aplicaciones escalables y fáciles de mantener. Sigue los principios de la programación orientada a objetos y los patrones de diseño, incluyendo los principios SOLID.

En este artículo os voy a enseñar como trabajar con SOLID, explicando cada apartado en que consiste y os daré diferentes ejemplos, sencillos pero a la vez claros para que entendiendo esto, podamos aplicarlo en cualquier proyecto que afrontemos.

¿Qué es SOLID?

Los principios SOLID son conjunto de principios que se utilizan en el desarrollo de software para hacer que estos diseños sean más comprensibles, flexibles, escalables y resistentes a los cambios.

Los Principios SOLID nos indicarán como organizar las funciones y estructuras de datos en los componentes y cómo estos componentes deben estar interconectados.

Lo más habitual en estos componentes es que sean clases, aunque esto no quiere decir que sea exclusivo solo de ellas, también se puede aplicar en más apartados. Los principios SOLID se pueden aplicar a cada producto de software.

De hecho deberían aplicarse aunque desgraciadamente no se suele hacer en varios casos.

Beneficios de usar SOLID

El principal beneficio de usar SOLID es que vamos a obtener un código que sea legible, fácilmente de mantener y testear.

Cuanto mejor código, la calidad de las colaboraciones será mayor y los desarrollos se realizarán con mayor agilidad y también con menor tendencia a complicaciones (y errores) con lo que dará opción a ampliar, modificar y probar los programas de una manera más sencilla.

Aparte de lo mencionado, utilizando este patrón va a permitir que nuestros proyectos cumpla estándares globales de la industria.

Si queréis profundizar más, os dejo un libro acerca de este tema, aplicando Clean Code en Javascript

Viendo estos beneficios, ¿Cuál es el problema de NO implementarlos? En muchos casos al ser un concepto nuevo y abstracto, puede parecer abrumador, crear y trabajar con un un código que cumple con los Principios SOLID. Es lo normal, sentirse así, y solo con esfuerzo conseguiremos estar en un estado confortable que hará que evitemos problemas y costos extra futuros, creando programas adaptables, eficaces, ágiles y sólidos en el tiempo.

Ahora que ya hemos visto los beneficios, vamos a trabajar al detalle con lo que es SOLID, lo que representa y sus detalles

SOLID es un acrónimo que representa

SOLID representa los siguientes conceptos.

  • S: Principio de Responsabilidad Única (Single Responsibility Principle, y podremos encontrarlo con estas siglas, SRP)
  • O: Principio de Abierto/Cerrado (Open / Close Principle, OCP)
  • L: Principio de Sustitución de Liskov ( Liskov Substitution Principle, LSP)
  • I: Principio de Segregación de Interfaces (Interface Segregation Principle, ISP)
  • D: Principio de Inversión de Dependencias (Dependency Inversion Principle, DIP)

Veamos cómo se pueden aplicar estos principios con NestJS.

1.- Principio de Responsabilidad Única (SRP)

Crearemos un controlador ProductController que tendrá una única responsabilidad: manejar las solicitudes HTTP relacionadas con los productos, sin mezclar ninguna responsabilidad más como poder obtener la lista de usuarios o realizar cualquier operación con las entidades de usuarios.

A continuación, os añado el código correspondiente al ejemplo de SRP para los productos:

import { Controller, Get, Post, Put, Delete } from '@nestjs/common';
import { ProductService } from './product.service'; // <=== Ahora se creará
@Controller('products')
export class ProductController {
constructor(private readonly productService: ProductService) {}
@Get()
async getProducts() {
return this.productService.getProducts();
}
@Post()
async createProduct() {
return this.productService.createProduct();
}
@Put(':id')
async updateProduct() {
return this.productService.updateProduct();
}
@Delete(':id')
async deleteProduct() {
return this.productService.deleteProduct();
}
}

2.- Principio de Abierto/Cerrado (OCP)

Crearemos un ProductService que manejará la lógica relacionada con los productos. Este servicio tendrá una interfaz IProductService que definirá el contrato al que el servicio debe cumplir.

Esto nos va a permitir hacer cambios de una manera sencilla en la implementación de ProductService por otra implementación que cumpla el mismo contrato.

import { Injectable } from '@nestjs/common';

export interface IProduct {
id: number;
name: string;
price: number;
}

export interface IProductService {
getProducts(): Promise<IProduct[]>;
createProduct(): Promise<IProduct>;
updateProduct(id: number): Promise<IProduct>;
deleteProduct(id: number): Promise<void>;
}

@Injectable()
export class ProductService implements IProductService {
private readonly products: IProduct[] = [];
async getProducts(): Promise<IProduct[]> {
return this.products;
}
async createProduct(): Promise<IProduct> {
// lógica para crear un producto
}
async updateProduct(id: number): Promise<IProduct> {
// lógica para actualizar un producto
}
async deleteProduct(id: number): Promise<void> {
// lógica para eliminar un producto
}
}

3.- Principio de Sustitución de Liskov (LSP)

Dado que ProductService implementa la interfaz IProductService, podemos intercambiar fácilmente su implementación por otra que cumpla el mismo contrato.

Por ejemplo, podríamos crear un MockProductService para usarlo con el objetivo de hacer que implemente la misma interfaz IProductService.

export class MockProductService implements IProductService {

private readonly products: IProduct[] = [
{ id: 1, name: 'Producto 1', price: 10 },
{ id: 2, name: 'Producto 2', price: 20 },
];

async getProducts(): Promise<IProduct[]> {
return this.products;
}

async createProduct(): Promise<IProduct> {
// lógica para crear un producto
}

async updateProduct(id: number): Promise<IProduct> {
// lógica para actualizar un producto
}

async deleteProduct(id: number): Promise<void> {
// lógica para eliminar un producto
}
}

4.- Principio de Segregación de Interfaces (ISP)

Hemos definido la interfaz IProductService para que sea específica a las necesidades del ProductController. Esto nos va a permitir crear una implementación de ProductService que cumpla el mismo contrato, pero también nos permite crear otras implementaciones de IProductService para otros propósitos, como una implementación de base de datos.

5. Principio de Inversión de Dependencias (DIP)

El Principio de Inversión de Dependencias (DIP) se puede aplicar aún más utilizando interfaces y clases abstractas para definir las dependencias. Esto nos permite programar en función de abstracciones en lugar de implementaciones concretas, lo que hace que nuestro código sea más flexible y adaptable a los cambios.

Por ejemplo, supongamos que queremos implementar un mecanismo de almacenamiento en caché para ProductService. Podemos crear una nueva interfaz, ICacheService, que defina el contrato para un servicio de caché.

export interface ICacheService<T> {
get(key: string): Promise<T>;
set(key: string, value: T): Promise<void>;
delete(key: string): Promise<void>;
}

Luego, podemos modificar el ProductService para que dependa de la interfaz ICacheService en lugar de una implementación concreta.

@Injectable()
export class ProductService implements IProductService {
private readonly products: IProduct[] = [];
constructor(private readonly cacheService: ICacheService<IProduct[]>) {}
async getProducts(): Promise<IProduct[]> {
const cachedProducts = await this.cacheService.get('products');
if (cachedProducts) {
return cachedProducts;
}
const products = await this.getProductsFromDatabase();
await this.cacheService.set('products', products);
return products;
}
// otros métodos de ProductService…
}

Luego, podemos crear un módulo de caché (CacheModule) que nos va a proporcionar una implementación concreta de la interfaz ICacheService, como RedisCacheService.

import { Module } from '@nestjs/common';
import { RedisCacheService } from './redis-cache.service';
import { ICacheService } from './cache.interface';
@Module({
providers: [
{
provide: ICacheService,
useClass: RedisCacheService,
},
],
})
export class CacheModule {}

Conclusión

Llegados a este punto podemos afirmar que el patrón SOLID es una herramienta fundamental para desarrollar aplicaciones sólidas y fáciles de mantener en NestJS (y otros lenguajes y tecnologías).

Al seguir los principios de la programación orientada a objetos (POO) y los patrones de diseño, podemos asegurarnos de que nuestro código sea flexible, escalable y fácil de entender.

A través de este artículo, hemos explorado cada uno de los principios SOLID y os he proporcionado ejemplos claros y sencillos para comprender cómo aplicarlos en nuestros proyectos.

Al dominar SOLID, tendremos la posibilidad de usar una metodología efectiva para construir aplicaciones de calidad y reducir la complejidad inherente a los proyectos de desarrollo, tanto en NestJS como en otras tecnologías.

La implementación de estos principios en cada componente de una aplicación permitirá una mayor cohesión, bajo acoplamiento y la posibilidad de realizar cambios y mejoras sin afectar al resto del sistema.

En resumen, al aplicar el patrón SOLID en NestJS, estamos estableciendo una base sólida para el desarrollo de aplicaciones escalables, fáciles de mantener y de alta calidad. Estos principios nos ayudan a crear un código limpio, modular y flexible, lo cual es fundamental para el éxito a largo plazo de nuestros proyectos.

Presencia en redes sociales

Podéis encontrarme en las siguientes redes.

--

--

Anartz Mugika Ledo🤗

[{#frontend:[#mobile:{#android, #kotlin, #ionic}}, {#web:{#angular, #qwik, #bootstrap}}],{#backend: [{#graphql, #nestjs,#express, #mongodb, #mysql}]}]