Índice de contenidos
- 1. Entorno
- 2. Introducción
- 3. Primeros pasos
- 4. Caso práctico API REST con acceso a base de datos
- 5. Conclusiones
1. Entorno
Este tutorial está escrito usando el siguiente entorno:
- Hardware: Slimbook Pro 13.3″ (Intel Core i7, 32GB RAM)
- Sistema Operativo: LUbuntu 16.04
- Visual Studio Code 1.16.1
- NodeJS v9.3.0
- NestJS v4.5.10
- Docker Compose 1.16.1
- Docker 18.02.0-ce
2. Introducción
El mundo de la consultoría si algo bueno tiene es que te permite conocer y trabajar con un montón de metodologías, tecnologías y herramientas de distinta naturaleza:
front, back, devops, agile…
Actualmente las tecnologías de codificación tanto del back como del front van convergiendo y los conceptos que tienes que tener en cuenta tanto en una como en otra cada vez son más parecidos, en buena parte gracias a que el front se ha ordenado y humanizado tras la llegada de TypeScript.
Ahora un full stack bastante cómodo de seguir por un desarrollador es el binomio Java (Spring Boot) – Angular que tanto se piden en las ofertas de empleo.
Hasta ahora la brecha más grande era cuando el front estaba en Angular (con su inyección de dependencias, sus decoradores, su estructura, …) y el back tenías que hacerlo en NodeJS, generalmente con Express para la creación de un API Rest y Socket.io para trabajar con Web Sockets, por defecto, implementando con JavaScript. Esto tiene la ventaja de que se tenga un único runtime (NodeJS) y se simplifique la gestión de la configuración de los proyectos así como los procesos de integración continua; pero también hace que un solo desarrollador tenga que cambiar mucho de chip para mantener las dos capas.
Digo hasta ahora porque ahora tenemos NestJS que es a NodeJS lo que Spring es a Java; un framework sobre NodeJS en TypeScript que te abstrae de la utilización de Express y Socket.io a través de decoradores, tiene inyección de dependencias «inspirada» en Angular y permite modularizar nuestras aplicaciones aplicando conceptos de orientación a objetos y programación funcional y reactiva.
3. Primeros pasos
Para empezar a «cacharrear» con el framework os recomiendo este repo que he creado, basado en el repo que viene en la documentación pero con la configuración necesaria para el testing.
1 2 3 4 |
$> git clone https://github.com/raguilera82/nestjs-seed.git tutorial-nest $> cd tutorial-nest $> npm install $> npm run start |
En este repositorio los tests unitarios y de integración están configurados con Jest y podemos lanzarlos con el comando:
1 |
$> npm run test |
Para arrancar la aplicación simplemente tenemos que ejecutar:
1 |
$> npm run start |
Eso es todo, ahora si accedes a la URL: http://localhost:3000 tendrás que ver el mensaje «NestJS Rocks» por pantalla.
Perfecto! Ya tenemos configurado el proyecto. Ahora vamos a abrirlo con nuestro editor favorito, en mi caso Visual Studio Code.
1 |
$> code . |
Vamos a ver porqué se ha pintado ese texto por pantalla. Si vamos al fichero «src/main.ts» vemos el arranque de la aplicación y donde podemos configurar para que el proyecto arranque en un puerto distinto al 3000.
1 2 3 4 5 6 7 8 |
import { NestFactory } from '@nestjs/core'; import { ApplicationModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(ApplicationModule); await app.listen(3000); } bootstrap(); |
En el fichero «src/app.module.ts» tenemos la definición del módulo principal a la que se hace referencia en el fichero «src/main.ts» y que, entre otros elementos, define los controladores que va a tener nuestro proyecto.
1 2 3 4 5 6 7 8 9 |
import { Module } from '@nestjs/common'; import { AppController } from './app.controller'; @Module({ imports: [], controllers: [AppController], components: [], }) export class ApplicationModule {} |
En el fichero «src/app.controller.ts» tenemos la definición del controlador al que se hace referencia en el fichero app.module.ts donde vemos como a través de decoradores se define el endpoint por defecto de tipo GET donde estamos devolviendo el texto que visualizamos por pantalla.
1 2 3 4 5 6 7 8 9 |
import { Get, Controller } from '@nestjs/common'; @Controller() export class AppController { @Get() root(): string { return 'NestJS Rocks'; } } |
Nota: A los que hayáis hecho algo con Angular esto os resultará profundamente familiar.
Como ahora estamos en modo de desarrollo vamos a arrancar el proyecto con el comando:
1 |
$> npm run start:watch |
De forma que cualquier cambio en el código provocará la recarga automática de la aplicación. Para hacer una prueba rápida simplemente modifica el texto «NestJS Rocks» por «NestJS loves Angular», verás en el terminal que el proyecto se recompila solo y, cuando refrescas el navegador, aparece el nuevo texto.
¡Bien! Mala pinta no tiene el entorno. 🙂
4. Caso práctico API REST con acceso a base de datos
Vamos a ver lo sencillo que es hacer un caso típico de acceso a base de datos para devolver los registros en formato JSON desde cero y con tests unitarios.
Todo el código y el entorno lo tienes preparado y listo para ejecutarse en el repositorio: https://github.com/raguilera82/tutorial-nest
Antes de meternos en el código vamos a preparar la base de datos para ello hacemos uso de Docker y Docker Compose y simplemente ejecutamos:
1 |
$> docker-compose up -d |
Esto se bajará la imagen de MySQL, creará la base de datos con la tabla «Tutorials» poblada con datos de prueba y dejará la instancia escuchando el puerto 3306. (Dentro de docker-compose.yml puedes ver los datos de acceso)
Para acceder a la base de datos MySQL vamos a utilizar TypeORM por lo que necesitamos incluir las siguientes dependencias:
1 |
$> npm install --save @nestjs/typeorm typeorm mysql |
Ahora creamos el fichero de configuración de TypeORM llamado «ormconfig.json» con la configuración de acceso a la base de datos y donde residen las entidades de la aplicación:
1 2 3 4 5 6 7 8 9 10 |
{ "type": "mysql", "host": "localhost", "port": 3306, "username": "dba", "password": "password", "database": "testdb", "entities": ["src/**/**.entity{.ts,.js}"], "synchronize": true } |
Ahora establecemos esta configuración en la aplicación editando el fichero «src/app.module.ts» para añadir el módulo de TypeORM.
1 2 3 4 5 6 7 8 9 |
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; @Module({ imports: [TypeOrmModule.forRoot()], controllers: [], components: [], }) export class ApplicationModule {} |
Nota:La función forRoot() busca el fichero de configuración ormconfig.json en el raíz del proyecto.
A fin de crear un módulo secundario con la funcionalidad de «tutorials» vamos a crear la carpeta «src/tutorials» donde vamos a ir almacenando el resto de elementos que componen este módulo.
Dado que estamos haciendo uso de TypeORM necesitamos crear una entidad que cumpla con los campos que queremos recuperar de la base de datos, dicha entidad la almacenamos en el fichero «tutorial.entity.ts»
con el siguiente contenido:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { Entity, PrimaryColumn, Column } from 'typeorm'; @Entity('Tutorials') export class Tutorial { @PrimaryColumn() id: number; @Column({length: 160}) name: string; @Column({length: 100}) author: string; } |
Nota muy importante: el nombre que se establece a través del decorador «Entity» debe ser exactamente igual al que se tenga en la base de datos, en caso contrario, creará una tabla vacía para esa entidad.
Con esta entidad vamos a configurar el módulo secundario, para lo cual vamos a crear el fichero «src/tutorials/tutorials.module.ts» donde vamos a configurar el repositorio de entidades a través del método forFeature()
1 2 3 4 5 6 7 8 9 10 11 |
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Tutorial } from './tutorial.entity'; @Module({ imports: [TypeOrmModule.forFeature([Tutorial])], components: [], controllers: [] }) export class TutorialsModule {} |
De esta forma ya podemos crear el servicio que se va a encargar de recuperar los datos; injectando a través del constructor una instancia del repositorio que entre otros métodos tiene find()
para recuperar todos los registros de una tabla, esta lógica la encapsulamos dentro de un async/await para manejar la asíncronicidad del lado del servidor con NodeJS.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { Component } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { Tutorial } from './tutorial.entity'; @Component() export class TutorialsService { constructor( @InjectRepository(Tutorial) private readonly tutorialsRepository: Repository<Tutorial>) {} async findAll(): Promise<Tutorial[]> { return await this.tutorialsRepository.find(); } } |
Nota: Esta clase no debería tener más lógica que recuperar los datos de la base de datos, dado que es más difícil de testear que cualquier otra clase de la aplicación al tener la dependencia con la base de datos y TypeORM.
Esta aplicación no tiene lógica más allá de la de devolver los datos de la tabla vía REST, por lo que no tiene sentido hacer un test unitario ni de integración para demostrar que TypeORM funciona. Lo siguiente que haremos será el controlador que exponga los datos.
Para ello dentro del directorio «tutorials» creamos el fichero «tutorials.controller.ts» donde vamos a injectar el servicio creado de recuperación y a través de anotaciones vamos a exponer los datos a través del endpoint «/tutorials».
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { Controller, Get } from '@nestjs/common'; import { Tutorial } from './tutorial.entity'; import { TutorialsService } from './tutorials.service'; @Controller('tutorials') export class TutorialsController { constructor(private readonly tutorialsService: TutorialsService) {} @Get() findAll(): Promise<Tutorial[]> { return this.tutorialsService.findAll(); } } |
Para probar el controlador lo mejor es que hagamos un fake del servicio de datos que devuelva la respuesta deseada. Para ello, vamos a crear el fichero «src/tutorials/tutorials.data.ts» que va a contener la constante TUTORIALS_DATA con la información fake de los tutoriales.
1 2 3 4 5 6 7 |
export const TUTORIALS_DATA = [ { "id": 1, "name": "Maquetación con Flexbox", "author": "César Alberca" }, { "id": 2, "name": "Primeros pasos con Jupyter Notebook", "author": "Gwydion Martín" }, { "id": 3, "name": "Introducción a NestJS", "author": "Rubén Aguilera" }, { "id": 4, "name": "Primeros pasos con Serenity y Cucumber", "author": "Santiago Toledano" }, { "id": 5, "name": "Registro dinámico de beans en el contexto de Spring", "author": "José Manuel Sánchez" } ]; |
Y a continuación creamos el servicio fake («src/tutorials/tutorials.service.fake.ts») que devuelve la respuesta,
donde no tenemos que hacer referencia a TypeORM.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { TUTORIALS_DATA } from './tutorials.data'; import { Component } from '@nestjs/common'; import { Tutorial } from './tutorial.entity'; @Component() export class TutorialsServiceFake { constructor() {} async findAll(): Promise<Tutorial[]> { return await TUTORIALS_DATA; } } |
De esta forma el test del controlador queda mucho más simple ya que gracias a la inyección de dependencias, como hacemos también en Angular, podemos cambiar la implementación real, dado que para el controlador no me importa de donde vengan los datos sino que hago con ellos.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import { Test } from '@nestjs/testing'; import { TutorialsController } from './tutorials.controller'; import { TUTORIALS_DATA } from './tutorials.data'; import { TutorialsService } from './tutorials.service'; import { TutorialsServiceFake } from './tutorials.service.fake'; describe('TutorialsController', () => { let tutorialsController: TutorialsController; let tutorialsService: TutorialsService; beforeEach(async () => { const module = await Test.createTestingModule({ controllers: [TutorialsController], components: [ {provide: TutorialsService, useClass: TutorialsServiceFake} ], }).compile(); tutorialsService = module.get<TutorialsService>(TutorialsService); tutorialsController = module.get<TutorialsController>(TutorialsController); }); describe('findAll', () => { it('should return an array of users', async () => { const result = TUTORIALS_DATA; expect(await tutorialsController.findAll()).toBe(result); }); }); }); |
Si ejecuto los tests:
1 |
$> npm run test |
Puedes ver que el test se ejecuta correctamente.
Una vez hecho esto solo resta registrar los elementos en los módulos correspondientes. En tutorials.module.ts damos de alta el controlador y el servicio con la implementación real.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Tutorial } from './tutorial.entity'; import { TutorialsController } from './tutorials.controller'; import { TutorialsService } from './tutorials.service'; @Module({ imports: [TypeOrmModule.forFeature([Tutorial])], components: [TutorialsService], controllers: [TutorialsController] }) export class TutorialsModule {} |
Y en app.module.ts importamos el módulo de «tutorials».
1 2 3 4 5 6 7 8 9 10 11 12 |
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AppController } from './app.controller'; import { TutorialsModule } from './tutorials/tutorials.module'; @Module({ imports: [TypeOrmModule.forRoot(), TutorialsModule], controllers: [AppController], components: [], }) export class ApplicationModule {} |
Arrancamos la aplicación con el comando:
1 |
$> npm run start |
Y navegando a la URL http://localhost:3000/tutorials veremos el listado de tutoriales dados de alta en la base de datos.
5. Conclusiones
Este es solo un pequeño caso de uso muy común que se puede resolver con este framework, te recomiendo que le eches un vistazo a la documentación oficial para ver todo su potencial.
El stack NestJS en el back y Angular en el front hace que no tengamos que cambiar mucho el chip para cambiar entre los dos mundos,
lo que resta misticidad a eso del desarrollador fullstack 😉
Cualquier duda o sugerencia en la zona de comentarios.
Saludos.