Como construir una API con Node.js y PostgreSQL

Como construir una API con Node.js y PostgreSQL

Emanuelly Leoncio. Nest.js es un framework Node.js que permite desarrollar aplicaciones backend eficientes, escalables y organizadas. Por defecto, usa TypeScript o JavaScript.

En este tutorial, crearemos una API con Nest.js, PostgreSQL y PrismaORM. Esta API simulará un sistema de tienda de ropa (store) y en él agregaremos y manipularemos registros de ropa (clothes) y sus marcas (brands). Trabajaremos con los endpoints GET, POST, PUT y DELETE.

Instalación y preparación del proyecto

En primer lugar, necesitarás tener Node y npm instalados en tu computadora. Con ellos instalados, podemos continuar con el tutorial.

Si no tienes Nest.js instalado, puedes ejecutar el siguiente comando en la terminal:

npm i -g @nestjs/cli


Ahora, comencemos un nuevo proyecto. Nuestra API se llamará store-nestjs.

nest new store-nestjs


Para abrir el proyecto en VSCode, ejecuta:

cd store-nestjs/


A continuación:

code .


Notarás que tenemos la siguiente estructura de carpetas:

src/
|-- main.ts
|-- app.module.ts
|-- app.controller.ts

|-- app.controller.spec.ts
|-- app.service.ts
|-- test/
|-- .env


Hablando brevemente de cada uno:

  • main.ts: punto de entrada donde se inicia la aplicación;
  • app.module.ts: módulo raíz, que importa y organiza todos los demás módulos;
  • app.controller.ts: responsable de manejar las solicitudes entrantes y devolver las respuestas al cliente;
  • app.service.ts: encapsula y gestiona la lógica de la aplicación. A través de él se puede reutilizar código, manteniendo una estructura organizada y desacoplada en toda la aplicación;
  • pasta test e app.controller.spec.ts: archivos de prueba;
  • .env: archivo utilizado para configurar variables de entorno.

Con VSCode abierto, eliminemos algunos archivos que no usaremos por ahora. Quitarlo:

  • La carpeta test;
  • Los archivos app.controller.spec.ts, app.controller.ts y app.service.ts.

Excluye también las importaciones, en app.module.ts, de los archivos anteriormente removidos:


El archivo app.module.ts quedara así:

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

@Module({
  imports: [],
  controllers: [],
  providers: [],
})
export class AppModule {}


PrismaORM

Prisma es una herramienta de mapeo relacional de objetos (ORM) y será responsable de crear y manipular la base de datos. Se puede acceder a su documentación aquí.

Para continuar con el tutorial, crearemos nuestra base de datos. Para ello instalaremos Prisma como dependencia de desarrollo.

npm install prisma --save-dev


Ahora, comencemos Prisma en el proyecto:

npx prisma init


Después de ejecutar el comando anterior, notarás que el archivo schema.prisma se ha insertado en el proyecto.


Configuración de la conexión de la base de datos

En el archivo .env, introduciremos los datos para conectarnos con la base. Las variables de entorno serán las siguientes:

DATABASE_URL="postgresql://SEU-USUARIO:SUA-SENHA@localhost:5432/NOME-BD?schema=public"


En:

  • SU-USUARIO: pon el usuario desde tu configuración de postgres;
  • SU-CONTRASEÑA: ingrese la contraseña para su configuración de postgres;
  • NOME-BD: introduce el nombre de la base de datos. Aquí la llamaremos store_nestjs.

Crea la base de datos usando el siguiente SQL. Aquí usaremos Beekeeper.

CREATE DATABASE store_nestjs;


Creación de las tablas

Primero vamos a crear la tabla brands. Nuestra tabla tendrá como atributos el id y el nombre de la marca.

En el archivo schema.prisma, agrega:

model Brand {
  id     Int      @id @default(autoincrement())
  name   String

  @@map("brands")
}


A continuación, crearemos la migration, un historial de lo que se crea y cambia en la base de datos. Entonces, ejecuta:

npx prisma migrate dev --name init


Ahora crearemos la tabla clothes. En esta tabla tendremos el id, el tipo y género de la ropa, un barcode y el ID de la marca, que será el enlace a la tabla brands.

Inserta en el schema.prisma:

model Brand {
  id     Int      @id @default(autoincrement())
  name   String
  Clothe Clothe[]

  @@map("brands")
}

model Clothe {
  id       Int     @id @default(autoincrement())
  type     String
  gender   String?
  bar_code String  @unique
  brand    Brand   @relation(fields: [brandId], references: [id])
  brandId  Int

  @@map("clothes")
}


Ejecuta nuevamente:

npx prisma migrate dev --name init


Observa que con cada cambio hecho se genera una nueva migration. Con esto ya tenemos todo el cronograma con cambios en nuestro banco.


Observa que en Beekeeper se generaron las tablas prisma_migrations, brands y clothes.


Creación del module

Vamos a crear un nuevo resource que traerá el controller, module y service para cada una de nuestras entidades, brand y clothe.

Ejecuta:

nest g resource modules/clothe


Durante la ejecución, aparecerán dos preguntas. Selecciona las siguientes opciones:


Realiza el mismo proceso con brand.

nest g resource modules/brand


Remueve los archivos de prueba según se ve en la siguiente figura:


Creación del DTO

Para la mejor organización del proyecto y para mantener las responsabilidades separadas, trabajaremos con el estándar DTO (Data Transfer Object). Crea los archivos brand.dto.ts y clothe.dto.ts en cada carpeta respectiva. En estos archivos detallaremos los datos a manipular y sus tipos (los mismos atributos que las tablas que creamos anteriormente).

Inserta este código en brand.dto.ts:

export type BrandDTO = {
  id: number;
  name: string;
};


En clothe.dto.ts:

export type ClotheDTO = {
  id: number;
  type: string;
  gender?: string;
  bar_code: string;
  brandId: number;
};



PrismaService

Crea el archivo prismaService.ts dentro de src:


Agrega el código:

import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect();
  }
}



Ahora importa este service creado en cada uno de los modules:


CRUD

Después de toda esta preparación, comencemos nuestro CRUD. Toda la lógica de la “regla de negocio” de nuestra aplicación estará concentrada en el servicio de cada entidad, en este caso, brand y clothe.

Endpoint POST

Para brand y clothe, crearemos una función asincrónica llamada create. En ella, primero comprobamos si los datos que se enviarán en el body de ka requisición ya existen. De ser así, devolverá un mensaje de error. De lo contrario, se crearán nuevos datos.

En brand tendremos:

import { Injectable } from '@nestjs/common';
import { BrandDTO } from './brand.dto';
import { PrismaService } from 'src/database/prismaService';

@Injectable()
export class BrandService {
  constructor(private prisma: PrismaService) {}

  async create(data: BrandDTO) {
    const brandExists = await this.prisma.brand.findFirst({
      where: {
        name: data.name,
      },
    });

    if (brandExists) {
      throw new Error('Brand already exists');
    }
    const brand = await this.prisma.brand.create({
      data,
    });

    return brand;
  }
}


En clothe:

import { Injectable } from '@nestjs/common';
import { ClotheDTO } from './clothe.dto';
import { PrismaService } from 'src/database/prismaService';

@Injectable()
export class ClotheService {
  constructor(private prisma: PrismaService) {}

  async create(data: ClotheDTO) {
    const clotheExists = await this.prisma.clothe.findFirst({
      where: {
        bar_code: data.bar_code,
      },
    });

    if (clotheExists) {
      throw new Error('Clothe already exists');
    }

    const clothe = await this.prisma.clothe.create({
      data,
    });

    return clothe;
  }
}


Enseguida, ajustamos los controllers de cada entidad, incluyendo post.

En clothe.controller:

import { Controller, Post, Body } from '@nestjs/common';
import { ClotheService } from './clothe.service';
import { ClotheDTO } from './clothe.dto';

@Controller('clothe')
export class ClotheController {
  constructor(private readonly clotheService: ClotheService) {}

  @Post()
  async create(@Body() data: ClotheDTO) {
    return this.clotheService.create(data);
  }
}


En brand.controller:

import { Body, Controller, Post } from '@nestjs/common';
import { BrandService } from './brand.service';
import { BrandDTO } from './brand.dto';

@Controller('brand')
export class BrandController {
  constructor(private readonly brandService: BrandService) {}

  @Post()
  async create(@Body() data: BrandDTO) {
    return this.brandService.create(data);
  }
}


Para dar start al proyecto, ejecuta:

npm run start:dev


Vamos a probar la ruta. En postman crearemos una marca en la ruta localhost:3000/brand:


Ahora, agregaremos algo de ropa, en la ruta localhost:3000/clothe:


Podemos ver en la base de datos que los registros fueron creados exitosamente:


Seguiremos igual con los demás endpoints.

Endpoint GET

En cada service, crearemos una función llamada findAll, que retornará todos los registros de la base de datos.

En clothe.service, añade:

async findAll() {
    return await this.prisma.clothe.findMany();
  }


En brand.service:

async findAll() {
    return await this.prisma.brand.findMany();
  }



En los controllers haremos la llamada para la ruta GET:

En brand.service:

@Get()
  async findAll() {
    return this.brandService.findAll();
  }


En clothe.service:

@Get()
  async findAll() {
    return this.clotheService.findAll();
  }


Ahora haremos la prueba en postman para ambas rutas:

En brand: localhost:3000/brand


En clothe: localhost:3000/clothe



Endpoint PUT

Esta ruta será la encargada de editar un registro. Para hacer esto, necesitamos pasar la identificación del registro como parámetro de ruta. Después de recibirlo, verificamos si esta identificación existe en el banco. Si es así, este registro se edita con la información pasada en el body de la requisición. Si no se encuentra, se devuelve un mensaje de error.

Insertamos esta lógica en el service de cada entidad.

En clothe.service, agrega:

async update(id: number, data: ClotheDTO) {
    const clotheExists = await this.prisma.clothe.findUnique({
      where: {
        id,
      },
    });

    if (!clotheExists) {
      throw new Error('Clothe does not exists.');
    }

    return await this.prisma.clothe.update({
      data,
      where: {
        id,
      },
    });
  }


En brand.service:

async update(id: number, data: BrandDTO) {
    const brandExists = await this.prisma.brand.findUnique({
      where: {
        id,
      },
    });

    if (!brandExists) {
      throw new Error('Brand does not exists.');
    }

    return await this.prisma.brand.update({
      data,
      where: {
        id,
      },
    });
  }


Finalmente, añade en el controller de cada uno la ruta put:

@Put(':id')
  async update(@Param('id') id: string, @Body() data: BrandDTO) {
    return this.brandService.update(Number(id), data);
  }

@Put(':id')
  async update(@Param('id') id: string, @Body() data: ClotheDTO) {
    return this.clotheService.update(Number(id), data);
  }


Para probar en postman, vamos a realizar la llamada pasando un id:

En clothe: localhost:3000/clothe/2



En brand: localhost:3000/brand/2


Endpoint Delete

En este último endpoint hay que prestar atención al siguiente detalle: toda la ropa está ligada a una marca. Así, estableceremos que una prenda no puede existir sin tener una marca vinculada a ella. Por lo tanto, al eliminar el registro de una marca, también se debe eliminar toda la ropa vinculada a ella. Los registros de ropa se pueden eliminar por separado.

Vamos a agregar esta regla a nuestros service:

En brand.service:

async delete(id: number) {
    const brandExists = await this.prisma.brand.findUnique({
      where: {
        id,
      },
    });

    if (!brandExists) {
      throw new Error('Brand does not exists.');
    }

    await this.prisma.clothe.deleteMany({
      where: {
        brandId: id,
      },
    });

    return await this.prisma.brand.delete({
      where: {
        id,
      },
    });
  }


En clothe.service:

async delete(id: number) {
    const clotheExists = await this.prisma.clothe.findUnique({
      where: {
        id,
      },
    });

    if (!clotheExists) {
      throw new Error('Clothe does not exists.');
    }

    return await this.prisma.clothe.delete({
      where: {
        id,
      },
    });
  }


En cada controller, agrega la ruta delete:

@Delete(':id')
  async delete(@Param('id') id: string) {
    return this.brandService.delete(Number(id));
  }

@Delete(':id')
  async delete(@Param('id') id: string) {
    return this.clotheService.delete(Number(id));
  }


Ahora realizaremos nuestras pruebas finales en postman.

En clothe: localhost:3000/clothe


En brand: localhost:3000/brand:


Así completamos con éxito nuestra solicitud.

Espero que esta guía haya sido de ayuda.

¡Éxito!

💡
Las opiniones y comentarios emitidos en este artículo son propiedad única de su autor y no necesariamente representan el punto de vista de Listopro.

Listopro Community da la bienvenida a todas las razas, etnias, nacionalidades, credos, géneros, orientaciones, puntos de vista e ideologías, siempre y cuando promuevan la diversidad, la equidad, la inclusión y el crecimiento profesional de los profesionales en tecnología.