Cómo realizar la colaboración entre controlador y servicio para implementar las típicas operaciones CRUD en un API REST desarrollada con Nest, de manera sencilla y con almacenamiento en memoria.
En este artículo vamos a ver un ejercicio sobre cómo organizar un controlador y un servicio en Nest. Será una práctica básica de funcionamiento de lo que sería un CRUD, pero sin persistencia, ya que el almacenamiento lo vamos a tener en la memoria.
El objetivo de esta práctica es ofrecer una estructura básica de lo que es un controlador con su servicio en Nest, que nos servirá más adelante para ir mejorando el servicio y controlador y aplicarle algunas cosas que no vamos a tocar ahora, como es la validación, la persistencia, etc.
Qué tenemos de antemano
No vamos a partir desde cero, dado que a lo largo del Manual de Nest hemos ido construyendo nuestra práctica actual, según íbamos aprendiendo lo que son los controladores y servicios.
De momento tenemos:
- El controlador
ProductsController
, que hasta ahora tenía solamente la ruta para recibir todos los productos y para insertar uno. - El servicio
ProductsService
, que tenía simplemente los métodos de recibir todos los elementos y modificar uno. - La interfaz
Product
que hemos usado para poder aplicar algo de tipado a la aplicación, definiendo los datos de producto.
Como decimos, sería ideal haber leído los artículos anteriores del manual para poder entender todo lo que nos sirve de código base. Además, algunas cosas sobre el código anterior van a cambiar, así que merece la pena estar atento a los motivos para aprender más cosas sobre cómo trabajan controladores y servicios.
Servicio completo
Vamos a comenzar viendo el código del servicio, ya que el controlador se apoya en el servicio. Esta clase contiene una interfaz pública que permite hacer las típicas operaciones de CRUD.
Voy a copiar todo el código de la clase y luego la comentaré.
import { Injectable } from '@nestjs/common';
import { Product } from './interfaces/product.interface';
@Injectable()
export class ProductsService {
private products: Product[] = [
{
id: 1,
name: 'Vela aromática',
description: 'Esta vela lanza ricos olores',
},
{
id: 2,
name: 'Marco de fotos pequeño',
description: 'Marco ideal para tus fotos 10x15',
}
];
getAll(): Product[] {
return this.products;
}
getId(id: number): Product {
return this.products.find( (item: Product) => item.id == id);
}
insert(body: any) {
this.products = [
...this.products,
{
id: this.lastId() + 1,
name: body.name,
description: body.description,
}
];
}
update(id: number, body: any) {
let product: Product = {
id,
name: body.name,
description: body.description,
}
this.products = this.products.map( (item: Product) => {
console.log(item, id, item.id == id);
return item.id == id ? product : item;
});
}
delete(id: number) {
this.products = this.products.filter( (item: Product) => item.id != id );
}
private lastId(): number {
return this.products[this.products.length - 1].id;
}
}
Primero señalar de nuevo que nos apoyamos en una interfaz para definir el tipo "Product".
import { Product } from './interfaces/product.interface';
Quiero volver a mencionar que esta interfaz es solo un preludio de lo que son las entidades, que nos permiten definir mejor los tipos, de cara al almacenamiento en base de datos. Como no hemos llegado a esa parte, tenemos suficiente de momento con una interfaz, que nos permite aportar tipos de TypeScript a nuestro código y así obtener mejores ayudas del editor y la detección temprana de errores.
Recuerda que los servicios son de las estructuras de Nest catalogadas como "Providers" y que por ello deben ser decoradas con @Injectable()
. Todo esto ya lo hemos visto anteriormente.
El servicio tiene una propiedad privada que es el array de productos. Sobre ese array, almacenado en memoria, es donde vamos a realizar las operaciones siguientes:
El método getAll()
Este método no ha sufrido ningún cambio con respecto a lo que teníamos, simplemente devuelve el array de productos tal cual.
Método getId(id: number)
Este método recibe el identificador que queremos recuperar y lo devuelve. Para encontar algo en un array usamos el método find() de Javascript.
Aquí es interesante mencionar que no estamos haciendo comprobaciones algunas sobre si ese elemento que se desea recuperar existe o no. Más adelante abordaremos este tipo de validaciones, que tendrán que realizarse en el servicio para liberar de trabajo a los controladores y evitar repetir código cuando varios controladores usen un mismo servicio.
Método insert(body: any)
Este método ha cambiado un poco, para adaptarnos a un modo de trabajo más realista. En realidad ahora lo que estamos recibiendo es el cuerpo de la solicitud, que enviaremos tal cual desde el controlador.
Dentro del método lo que hacemos es crear ese nuevo producto, construyendo el objeto Product directamente en el servicio. Para construirlo nos apoyamos además en un método nuevo, privado, que hemos creado en el servicio, que nos dice el último id usado: this.lastId(). Así podemos asegurarnos que el próximo producto siempre tendrá un id superior al último creado.
Podría darse el caso que los datos del body no contengan toda la información necesaria para crear un producto. Este supuesto no lo estamos validando todavía.
Método update(id: number, body: any)
Este método es totalmente nuevo y nos permite actualizar un producto cuyo id se recibe por parámetro, con respecto a un contenido, que es el mismo body enviado desde el controlador.
En esta acción usamos el método map() de los arrays para construir un array con el contenido que había antes, pero sustituyendo el producto con el id a actualizar por un nuevo objeto construido con los datos del body enviado.
Método delete(id: number)
Por último, el método delete() nos permite borrar un elemento del array de productos, el elemento con el id recibido por parámetro.
Para borrar usamos el metodo filter() de los arrays de Javascript, donde nos quedamos con todos los elementos del array menos con el que tiene el id que queremos borrar.
Método private lastId(): number
Luego tenemos también el método privado lastId() que nos devuelve el id del último elemento del array. Es privado porque en principio solamente lo necesito usar dentro del servicio.
Como hemos señalado este servicio es bastante elemental. Faltarían muchas cosas que ver, principalmente las validaciones que habrá que hacer en esta misma clase. Más adelante trabajaremos con ellas.
Controlador de producto
Ahora vamos a ver el controlador, que hará uso de nuestro servicio. El conocimiento necesario para poder realizarlo lo hemos adquirido a lo largo de los anteriores artículos del manual. Ahora vamos a incorporar nuevos métodos y algunas diferencias menores en el trabajo de los que ya teníamos.
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
import { Product } from './interfaces/product.interface';
import { ProductsService } from './products.service';
@Controller('products')
export class ProductsController {
constructor(private readonly productsService: ProductsService) { }
@Get()
getAllProducts(): Product[] {
return this.productsService.getAll();
}
@Get(':id')
find(@Param('id') id: number) {
return this.productsService.getId(id);
}
@Post()
@HttpCode(HttpStatus.NO_CONTENT)
createProduct(
@Body() body,
) {
this.productsService.insert(body);
}
@Put(':id')
update(
@Param('id') id: number,
@Body() body,
) {
return this.productsService.update(id, body);
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
delete(@Param('id') id: number) {
this.productsService.delete(id);
}
}
El controlador es bastante sencillo y sistemático. La mayoría de los detalles ya los debes de entender porque los hemos explicado antes, así que voy directamente a comentar las novedades con respecto a códigos anteriores.
El método que realiza gestiona el request tipo POST para la inserción, createProduct(), ahora simplemente manda el body de la solicitud al servicio, que debería encargarse de procesarla y hacer la inserción correctamente.
El método update(), que gestiona la solicitud PUT, funciona de manera similar al createProduct(). Tampoco se encarga de crear el objeto producto, sino que envía el body de la solicitud directamente al servicio.
El método delete() que gestiona la ruta para eliminar un producto, delega también en el servicio, enviando simplemente el identificador del elemento a eliminar.
Conclusión
Con esto hemos completado un ejercicio básico de creación de un controlador y servicio en Nest. Ha sido todo muy simple y nos faltan muchas cosas para hacer para que sea una práctica realista, pero al menos ha resultado de ayuda para ver cómo se comunican estas dos piezas de software para cubrir los métodos esenciales para hacer un CRUD.
Más adelante vamos a mejorar el código presente, para ir agregando algunas mejoras necesarias para hacer una aplicación más robusta, comenzando por la creación de algunas comprobaciones básicas y tratamiento de errores en el servicio.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...