Api Rest NestJS Mongodb
Api Rest NestJS Mongodb
1
Índice
1. Introducción
3. Insomnia
4. NestJs
5. MongoDB
6. Desarrollo de la API
7. Ejercicios propuestos
2
Introducción
En este breve manual se describe cómo desarrollar una API REST muy simple usando NestJs y MongoDB en
cinco sencillos pasos, creando los endpoints (puntos de acceso a la API) necesarios para obtener, añadir,
borrar y modificar elementos.
Se toma como ejemplo el sujeto tarea. Cada una de las tareas tiene un título, una descripción y, además,
guarda información sobre si está o no está hecha. A continuación se muestra una tarea de ejemplo en
formato JSON:
{
"title": "Comprar pan",
"description": "Comprar pan de molde integral multicereales.",
"done" : false,
}
Además de esta información, cuando una tarea se guarda en la base de datos, se le asigna de forma
automática un identificador único.
3
¿Qué es una API REST?
Las siglas API vienen de Application Programming Interface que se puede traducir al español como Interfaz
de programación de aplicaciones.
En pocas palabras, se puede decir que una API es un software que hace de intermediario o puente para que
dos aplicaciones (o dos partes de una misma aplicación) se puedan comunicar. Es típico tener una API que
permite comunicar el front-end con el back-end.
Las siglas REST vienen de Representational State Transfer y denotan el tipo de API. La característica más
destacable de las API REST es la flexibilidad en cuanto al formato de los datos que se transfieren, que
puede ser XML, JSON, YAML o cualquier otro.
FRONT-END BACK-END
PETICIONES A LA API
Con un ejemplo lo veremos más claro. Imaginemos que desde el front-end se quiere sacar un listado de
todas las tareas (tasks). El procedimiento a seguir, a grandes rasgos, sería el siguiente:
1. Desde el front-end se hace una petición HTTP al endpoint tasks (la URL completa sería algo como
http://myapplication.com/tasks/) de la API.
2. El back-end devuelve un array con todas las tareas en formato JSON (o en cualquier otro formato)
junto con el código 200 que indica que todo ha ido bien y no se ha producido ningún error.
3. El front-end toma ese array y muestra el contenido por pantalla convenientemente maquetado.
4
Insomnia
Insomnia es un programa que permite hacer peticiones HTTP sin necesidad de programar un front-end y es
muy útil para hacer pruebas cuando se está desarrollando una API.
Puede que el lector conozca Postman, que es seguramente el programa más popular de este tipo. En el
presente manual hemos elegido Insomnia por su sencillez.
5
NestJs
Según la descripción que encabeza la web oficial, NestJs (o simplemente Nest) es un framework basado en
Node.js para crear aplicaciones del lado del servidor eficientes, fiables y escalables.
Para instalar Nest, hay que teclear lo siguiente en una ventana de terminal:
6
MongoDB
MongoDB es una base de datos noSQL, distribuida y está basada en documentos. Encaja muy bien en
aplicaciones en las que se usa como lenguaje javascript o typescript ya que el formato de los datos que se
guardan en esta base de datos es JSON.
Existe una versión gratuita que se puede instalar de forma local llamada Community Server y una versión
en la nube denominada Atlas.
7
Desarrollo de la API
Paso 1 - Creación del proyecto
Nest permite hacer scaffolding, es decir, crear código mediante comandos. Para ello se utiliza el comando
nest seguido de una serie de opciones. Para ver todas las posibilidades que tenemos a golpe de tecla,
podemos ejecutar nest --help.
8
Una vez creado el proyecto, lanzamos el servidor:
cd tasks-api
npm run start:dev
nest g resource
9
Le damos como nombre al recurso tasks. Elegimos REST API y decimos que sí (Y) queremos que se
creen los puntos de entrada a la API.
Vemos que dentro de la carpeta src, donde está el código fuente del proyecto, se ha creado otra carpeta
con nombre tasks con el siguiente contenido:
tasks
├── dto
│ ├── create-task.dto.ts
│ └── update-task.dto.ts
├── entities
│ └── task.entity.ts
├── tasks.controller.spec.ts
├── tasks.controller.ts
├── tasks.module.ts
├── tasks.service.spec.ts
└── tasks.service.ts
Sin tocar una línea de código ya se pueden ir probando los endpoints o puntos de entrada a la API que se
han creado por defecto.
10
O, mejor aún, podemos probar con Insomnia:
Mongoose es un ODM (Object Data Modeling) para MongoDB. Es el equivalente a un ORM pero sobre una
base de datos noSQL documental. Igual que con un ORM, todas las acciones sobre la base de datos se
realizan de forma indirecta mediante el ODM que es el encargado de ejecutar internamente comandos
nativos de MongoDB.
@Module({
imports: [
TasksModule,
MongooseModule.forRoot('mongodb://localhost/tasksdb')
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Hemos especificado una conexión a una base de datos con nombre tasksdb que todavía no existe. El
propio Mongoose se encargará de crearla cuando sea necesario.
En un modelo relacional, un esquema sería algo así como la definición de una tabla. En MongoDB, en
principio, no hay que declarar la estructura de los documentos ya que se trata, por definición, de una base
de datos desestructurada.
En nuestro caso, como estamos trabajando con Mongoose, cada esquema definido se va a mapear a una
colección de MongoDB y va a definir la estructura de esa colección.
src
├── app.controller.spec.ts
├── app.controller.ts
├── app.module.ts
├── app.service.ts
├── main.ts
├── schemas
│ └── task.schema.ts
└── tasks
├── dto
│ ├── create-task.dto.ts
│ └── update-task.dto.ts
├── entities
12
│ └── task.entity.ts
├── tasks.controller.spec.ts
├── tasks.controller.ts
├── tasks.module.ts
├── tasks.service.spec.ts
└── tasks.service.ts
Copiamos el código que viene en la documentación de Nest y lo adaptamos a nuestro gusto para que en
lugar de representar gatos, represente tareas.
@Schema()
export class Task {
@Prop()
title: string;
@Prop()
description: string;
@Prop()
done: boolean;
}
Para que el esquema esté accesible, es necesario importarlo desde el módulo correspondiente.
@Module({
imports: [MongooseModule.forFeature([{ name: Task.name, schema:
TaskSchema }])],
controllers: [TasksController],
providers: [TasksService]
})
export class TasksModule {}
13
Paso 5 - Adaptación del servicio
En este paso, vamos a conseguir que el servicio interactúe con la base de datos. Para ello, deberemos
añadir y modificar algo de código basándonos en la documentación oficial de Nest. Se trata del paso más
largo, aunque nada complicado como podrá comprobar el lector.
Ahora los métodos create() y findAll() devuelven sendas cadenas de texto. Hay que modificarlos
para que create() añada una nueva tarea a la base de datos y findAll() devuelva una lista con todas
las tareas existentes.
Para saber más sobre los métodos disponibles aplicables a los modelos, se puede consultar la
documentación de Mongoose en la siguiente dirección: https://mongoosejs.com/docs/models.html
@Injectable()
export class TasksService {
constructor(@InjectModel(Task.name) private taskModel:
Model<TaskDocument>) {}
14
async create(createTaskDto: CreateTaskDto): Promise<Task> {
const createdTask = new this.taskModel(createTaskDto);
return createdTask.save();
}
findOne(id: number) {
return `This action returns a #${id} task`;
}
remove(id: number) {
return `This action removes a #${id} task`;
}
}
En estos momentos, aunque la API no esté completa, ya se puede ir probando. Vamos a lanzar una petición
POST al servidor con Insomnia. No hay que olvidar que la aplicación debe estar funcionando (se lanza con
npm run start:dev).
Al hacer POST, se debería haber creado una tarea. Lo que sucede es que no hemos dado ningún dato para
esa tarea, ni el título ni la descripción ni la información sobre si está o no realizada ¿se habrá grabado algo?
vamos a averiguarlo.
mongo
15
Una vez dentro, comprobamos qué ha sucedido:
¡Estupendo! Se ha creado una base de datos con nombre tasksdb y dentro de ella hay una colección que
se llama tasks que contiene una tarea aunque, de momento con un identificador como único dato.
Vamos a crear ahora un par de tareas con datos de verdad. En el cuerpo (Body) de la petición POST
enviamos los datos en formato JSON.
Hemos creado la tarea "Comprar pan". Observa que el código devuelto cuando se crea correctamente un
objeto es el 201.
16
Comprobamos que se han insertado correctamente las tareas en la base de datos:
> db.tasks.find().pretty()
{ "_id" : ObjectId("60f29944a556a02024cc2636"), "__v" : 0 }
{
"_id" : ObjectId("60f2a1cba556a02024cc2638"),
"title" : "Comprar pan",
"description" : "Comprar pan de molde integral multicereales.",
"done" : false,
"__v" : 0
}
{
"_id" : ObjectId("60f2a237a556a02024cc263a"),
"title" : "Ir al gimnasio",
"description" : "Hacer pecho y bíceps.",
"done" : true,
"__v" : 0
}
Además del método create(), tenemos implementado en el servicio el método findAll(). Este último
método sirve para obtener todas las tareas. Lo podemos probar haciendo una petición GET.
17
En efecto, al realizar una petición GET en la URL http://localhost:3000/tasks obtenemos un array de todas
las tareas creadas.
El id en MongoDB puede contener letras y números por lo que debemos hacer un pequeño ajuste en el
método findOne() del controlador tasks.controller.ts para que no se produzcan errores con los
tipos de datos, ya que por defecto, considera los id como números.
@Get(':id')
findOne(@Param('id') id: string) {
return this.tasksService.findOne(id);
}
18
Por último, reescribimos los métodos update() y remove() del servicio:
Del mismo modo que hicimos anteriormente, debido a que el id puede contener caracteres, actualizamos
también los dos métodos correspondientes del controlador tasks.controller.ts:
@Patch(':id')
update(@Param('id') id: string, @Body() updateTaskDto: UpdateTaskDto) {
return this.tasksService.update(id, updateTaskDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.tasksService.remove(id);
}
Probamos una actualización haciendo una petición PATCH y escribiendo en el cuerpo el atributo que
queremos cambiar en formato JSON. Cambiaremos el valor de description dejando los demás datos
igual:
19
Al mostrar un listado de todas las tareas de la base de datos, vemos que se ha modificado la descripción de
la tarea que queríamos:
> db.tasks.find().pretty()
{ "_id" : ObjectId("60f29944a556a02024cc2636"), "__v" : 0 }
{
"_id" : ObjectId("60f2a1cba556a02024cc2638"),
"title" : "Comprar pan",
"description" : "Comprar pan de centeno.",
"done" : false,
"__v" : 0
}
{
"_id" : ObjectId("60f2a237a556a02024cc263a"),
"title" : "Ir al gimnasio",
"description" : "Hacer pecho y bíceps.",
"done" : true,
"__v" : 0
}
Vamos a borrar esa primera tarea que creamos al principio y que no contenía ningún dato relevante aparte
del id usando una petición HTTP de tipo DELETE:
20
Comprobamos que se ha borrado:
> db.tasks.find().pretty()
{
"_id" : ObjectId("60f2a1cba556a02024cc2638"),
"title" : "Comprar pan",
"description" : "Comprar pan de centeno.",
"done" : false,
"__v" : 0
}
{
"_id" : ObjectId("60f2a237a556a02024cc263a"),
"title" : "Ir al gimnasio",
"description" : "Hacer pecho y bíceps.",
"done" : true,
"__v" : 0
}
@Injectable()
export class TasksService {
constructor(@InjectModel(Task.name) private taskModel:
Model<TaskDocument>) {}
21
return this.taskModel.findByIdAndDelete(id);
}
}
22
Ejercicios propuestos
Ejercicio 1
Además de los datos title, description y done, ahora cada tarea también debe disponer de
minutes que es el tiempo estimado en minutos que debe tomar la tarea en realizarse. Comprueba que se
pueden crear, borrar y modificar tareas teniendo en cuenta esta mejora.
Ejercicio 2
Implementa dos nuevos endpoints, a saber, tasks/done que devuelve todas las tareas realizadas y
tasks/todo que devuelve todas las que quedan por hacer.
Ejercicio 3
Añade un nuevo endpoint que permita hacer una búsqueda por título, es decir, que sea capaz de obtener
una lista de todas las tareas cuyo título contiene una cadena determinada, sin distinguir mayúsculas y
minúsculas. El formato es tasks/title/(tasktitle). Por ejemplo, tasks/title/pan debería
mostrar las tareas con títulos como "Ir a la panadería", "Comprar pan", "Pan y queso" o "Acampando en el
camping".
Ejercicio 4
Implementa el endpoint tasks/longtasks de forma que devuelva todas las tareas cuyo tiempo
estimado de realización sea mayor o igual a una hora (60 minutos).
23
Soluciones a los ejercicios
Ejercicio 1
Añadir un atributo más a la tarea es tan sencillo como poner una propiedad más en el esquema, tan solo
hay que añadir lo siguiente:
@Prop()
minutes: number;
@Schema()
export class Task {
@Prop()
title: string;
@Prop()
description: string;
@Prop()
done: boolean;
@Prop()
minutes: number;
}
Para comprobar que todo sigue funcionando bien, podemos crear una nueva tarea haciendo una petición
POST:
24
Comprobamos que en la base de datos se ha insertado correctamente la tarea con todos los datos, incluido
el tiempo en minutos previsto para su realización:
$ mongo
>
> use tasksdb
switched to db tasksdb
>
> db.tasks.find().pretty()
{
"_id" : ObjectId("60f2a1cba556a02024cc2638"),
"title" : "Comprar pan",
"description" : "Comprar pan de centeno.",
"done" : false,
"__v" : 0
}
{
"_id" : ObjectId("60f2a237a556a02024cc263a"),
"title" : "Ir al gimnasio",
"description" : "Hacer pecho y bíceps.",
"done" : true,
"__v" : 0
}
{
"_id" : ObjectId("60f67d3774a3b822865d9481"),
"title" : "Ordenar escritorio",
"description" : "Poner cada cosa en su sitio.",
"done" : false,
"minutes" : 20,
"__v" : 0
}
Ejercicio 2
Los endpoints que tienen como finalidad obtener elementos (en este caso son tareas) se deben gestionar
mediante peticiones GET.
25
Modificamos el fichero tasks.controller.ts que es el controlador encargado de manejar las
peticiones HTTP añadiendo dos nuevos métodos:
@Get('done')
findDone() {
return this.tasksService.findDone();
}
@Get('todo')
findTodo() {
return this.tasksService.findTodo();
}
Es importante colocar esos dos métodos antes de findAll() que tiene el decorador @Get().
Los métodos findDone() y findTodo() llaman a los métodos homónimos que se encuentran en el
servicio implementado en el fichero tasks.service.ts y que son los encargados de realizar las
consultas con Mongoose para obtener las tareas.
Ahora probamos el endpoint tasks/todo para obtener todas las tareas por hacer:
26
Ejercicio 3
Modificamos el controlador tasks.controller.ts añadiendo el método findWithTitle():
@Get('title/:searchString')
findWithTitle(@Param('searchString') searchString: string) {
return this.tasksService.findWithTitle(searchString);
}
Para la búsqueda, hacemos uso de una expresión regular indicando la opción de no distinguir mayúsculas
de minúsculas.
Hacemos la prueba pertinente probando la búsqueda de la cadena "ar" en el título de las tareas:
27
Ejercicio 4
Seguimos el mismo procedimiento para añadir el endpoint tasks/longtasks, es decir, primero
añadimos el método en el controlador y luego en el servicio.
Para realizar la comparación "mayor o igual a" se utiliza $gte (greater than or equal).
@Controller('tasks')
export class TasksController {
constructor(private readonly tasksService: TasksService) { }
@Post()
create(@Body() createTaskDto: CreateTaskDto) {
return this.tasksService.create(createTaskDto);
}
@Get('done')
findDone() {
return this.tasksService.findDone();
}
@Get('todo')
findTodo() {
return this.tasksService.findTodo();
28
}
@Get('title/:searchString')
findWithTitle(@Param('searchString') searchString: string) {
return this.tasksService.findWithTitle(searchString);
}
@Get('longtasks')
findLongTasks() {
return this.tasksService.findLongTasks();
}
@Get()
findAll() {
return this.tasksService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.tasksService.findOne(id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateTaskDto: UpdateTaskDto) {
return this.tasksService.update(id, updateTaskDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.tasksService.remove(id);
}
}
A continuación se muestra el contenido completo del fichero tasks.service.ts que incluye el nuevo
método findLongTasks():
@Injectable()
export class TasksService {
constructor(@InjectModel(Task.name) private taskModel:
Model<TaskDocument>) {}
29
}
Hasta el momento no hemos añadido tareas de una hora o más, así que creamos la tarea "Escribir artículo"
con una duración de 120 minutos:
30
Comprobamos que mediante el endpoint tasks/longtasks se muestra la tarea correcta, la que dura 120
minutos y no "Ordenar el escritorio" que tan solo dura 20 minutos.
31
Otros libros del autor
Aprende Java con Ejercicios
"Aprende Java con Ejercicios" es un manual práctico para aprender a programar en Java desde cero. Es un
libro hecho casi a medida de la asignatura Programación que forma parte del currículo del primer curso de
los ciclos formativos DAW (Desarrollo de Aplicaciones Web) y DAM (Desarrollo de Aplicaciones
Multiplataforma) pero igualmente puede ser utilizado por estudiantes de Ingeniería Informática, Ingeniería
de Telecomunicaciones o Ciencias Matemáticas en la asignatura de Programación de los primeros cursos.
Incluye conceptos como variables, tipos de datos, sentencia condicional, bucles, arrays, Programación
Orientada a Objetos, manejo de ficheros, control de excepciones, JSP, acceso a bases de datos desde Java y
mucho más. Contiene más de 300 ejercicios resueltos.
Este libro puede descargarse de forma gratuita desde el siguiente enlace: https://leanpub.com/gitygithub
32
Datos de contacto
Puedes ponerte en contacto con Luis José Sánchez mediante LinkedIn
(https://www.linkedin.com/in/luisjosesanchez) y también lo puedes seguir en Twitter (@luisjoseprofe).
33