Como construir uma API utilizando Node.js e PostgreSQL

Como construir uma API utilizando Node.js e PostgreSQL

Emanuelly Leoncio. O Nest.js é um framework Node.js que permite desenvolver aplicações backend eficientes, escaláveis e organizadas. Por padrão, utiliza-se o TypeScript ou JavaScript.

Neste tutorial, iremos construir uma API com Nest.js, PostgreSQL e PrismaORM. Essa API irá simular um sistema de loja de roupas (store), e nele iremos adicionar e manipular os registros de roupas (clothes) e suas marcas (brands). Trabalharemos com os endpoints GET, POST, PUT e DELETE.

Instalação e preparação do projeto

Primeiramente, você precisará ter instalado em seu computador, o Node e npm. Com eles instalados, podemos dar seguimento ao tutorial.

Caso não tenha o Nest.js instalado, você poderá executar o seguinte comando no terminal:

npm i -g @nestjs/cli


Agora, vamos iniciar um novo projeto. Nossa API irá se chamar store-nestjs.

nest new store-nestjs


Para abrir o projeto no VSCode, rode:

cd store-nestjs/


E, em seguida:

code .


Você notará que temos a seguinte estrutura de pastas:

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

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


Falando brevemente sobre cada um:

  • main.ts: ponto de entrada, onde a aplicação é inicializada;
  • app.module.ts: módulo raiz, que importa e organiza todos os outros módulos;
  • app.controller.ts: responsável ​​por lidar com as solicitações recebidas e retornar as respostas ao cliente;
  • app.service.ts: encapsulam e gerenciam a lógica da aplicação. Por meio dele, pode-se reutilizar código, mantendo uma estrutura organizada e desacoplada em todo o aplicativo;
  • pasta test e app.controller.spec.ts: arquivos de testes;
  • .env: arquivo utilizado para configuração das variáveis de ambiente.

Com o VSCode aberto, vamos remover alguns arquivos que não iremos utilizar por hora. Remova:

  • A pasta test;
  • Os arquivos app.controller.spec.ts, app.controller.ts e app.service.ts.

Exclua também as importações, no app.module.ts, dos arquivos anteriormente removidos:


O app.module.ts ficará assim:

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

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


PrismaORM

O Prisma é uma ferramenta de mapeamento objeto-relacional (ORM), e será responsável pela criação e manipulação do banco de dados. A documentação dele pode ser acessada aqui.

Para darmos prosseguimento ao tutorial, iremos criar o nosso banco de dados. Para isso, instalaremos o prisma como dependência de desenvolvimento.

npm install prisma --save-dev


Agora, vamos iniciar o prisma no projeto:

npx prisma init


Após rodar o comando acima, você notará que o arquivo schema.prisma foi inserido no projeto.


Configuração da conexão do banco de dados

No arquivo .env, iremos inserir os dados para conexão com o banco. As variáveis de ambiente ficarão da seguinte forma:

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


Em:

  • SEU-USUARIO: coloque o usuário da sua configuração do postgres;
  • SUA-SENHA: coloque a senha da sua configuração do postgres;
  • NOME-BD: coloque o nome do banco de dados. Aqui chamaremos de store_nestjs.

Crie o banco de dados por meio do sql abaixo. Aqui estaremos utilizando o Beekeeper.

CREATE DATABASE store_nestjs;


Criação das tabelas

Primeiramente, vamos criar a tabela brands. Nossa tabela terá como atributos o id e o nome da marca.

No arquivo schema.prisma, adicione:

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

  @@map("brands")
}


Em seguida, vamos criar a migration, que se trata de um histórico do que está sendo criado e alterado no banco de dados. Assim, execute:

npx prisma migrate dev --name init


Agora, vamos criar a tabela clothes. Nesta tabela, teremos o id, o tipo e gênero da roupa, um barcode, e o id da marca, que será o vínculo com a tabela brands.

Insira no 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")
}


Execute novamente:

npx prisma migrate dev --name init


Observe que a cada alteração feita, uma nova migration é gerada. Com isso, temos toda a linha do tempo com as alterações do nosso banco.


Observe que no Beekeeper, foi gerado as tabelas prisma_migrations, brands e clothes.


Criação do module

Vamos criar uma nova resource, que irá trazer o controller, module e service para cada uma das nossas entidades, o Brand e o Clothe.

Execute:

nest g resource modules/clothe


Durante a execução irão aparecer duas perguntas. Selecione as opções abaixo:


Realize o mesmo processo para brand.

nest g resource modules/brand


Remova os arquivos de teste, conforme indicado na figura abaixo:


Criação do DTO

Para a melhor organização do projeto, e manter as responsabilidades separadas, iremos trabalhar com o padrão DTO (Data Transfer Object). Crie os arquivos brand.dto.ts e clothe.dto.ts em cada respectiva pasta. Nesses arquivos, iremos detalhar quais serão os dados a serem manipulados e seus tipos (os mesmos atributos das tabelas que criamos anteriormente).

Em brand.dto.ts, insira o código:

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


Em clothe.dto.ts:

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



PrismaService

Crie o arquivo prismaService.ts dentro de src:


Adicione o 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();
  }
}



Agora, importe este service criado em cada um dos modules:


CRUD

Após toda esta preparação, vamos iniciar nosso CRUD de fato. Toda a lógica da “regra de negócio” da nossa aplicação ficará concentrada no service de cada entidade, no caso, brand e clothe.

Endpoint POST

Para brand e clothe, criaremos uma função assíncrona denominada create. Nela, primeiramente verificamos se o dado, que será enviado no body da requisição, já existe. Caso sim, retornará uma mensagem de erro. Se não, o novo dado será criado.

Em Brand, teremos:

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;
  }
}


Em 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;
  }
}


Em seguida, ajustamos os controllers de cada entidade, incluindo a chamada post.

Em 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);
  }
}


Em 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 o start no projeto, execute:

npm run start:dev


Vamos testar a rota. No postman, vamos criar uma marca na rota localhost:3000/brand:


Agora, adicionaremos algumas roupas, na rota localhost:3000/clothe:


Podemos observar no banco de dados, que os registros foram criados com sucesso:


Seguiremos da mesma forma para os demais endpoints.

Endpoint GET

Em cada service, iremos criar uma função chamada findAll, que irá retornar todos os registros do banco de dados.

No clothe.service, adicione:

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


No brand.service:

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



Nos controllers, faremos a chamada para a rota GET:

Em brand.service:

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


Em clothe.service:

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


Agora, faremos o teste no postman para ambas as rotas:

Em brand: localhost:3000/brand


Em clothe: localhost:3000/clothe



Endpoint PUT

Esta rota será responsável por editar um registro. Para isso, precisamos passar o id do registro como parâmetro da rota. Após recebê-lo, verificamos se existe este id no banco. Caso positivo, este registro é editado com as informações passadas no body da requisição. Caso não seja encontrado, uma mensagem de erro é retornada.

Inserimos esta lógica no service de cada entidade.

No clothe.service, adicione:

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,
      },
    });
  }


No brand.service, insira:

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,
      },
    });
  }


Por fim, adicione no controller de cada um a rota 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 testar, no postman, vamos realizar a chamada, passando um id:

Em clothe: localhost:3000/clothe/2



Em brand: localhost:3000/brand/2


Endpoint Delete

Neste último endpoint, precisamos nos atentar ao seguinte detalhe: toda roupa é vinculada a uma marca. Assim, estabeleceremos que uma roupa não poderá existir sem ter uma marca vinculada a ela. Desta forma, ao remover o registro de uma marca, todas as roupas vinculadas a ela também deverão ser deletadas. Já os registros das roupas poderão ser deletados separadamente.

Vamos adicionar esta regra aos nossos service:

No 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,
      },
    });
  }


No clothe.service, adicione:

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,
      },
    });
  }


Em cada controller, adicione a rota 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));
  }


Agora, vamos realizar os nossos testes finais no postman.

Em clothe: localhost:3000/clothe


Em brand: localhost:3000/brand

Assim, finalizamos nossa aplicação com êxito!

Sucesso!

💡
As opiniões e comentários expressos neste artigo são de propriedade exclusiva de seu autor e não representam necessariamente o ponto de vista da Revelo.

A Revelo Content Network acolhe todas as raças, etnias, nacionalidades, credos, gêneros, orientações, pontos de vista e ideologias, desde que promovam diversidade, equidade, inclusão e crescimento na carreira dos profissionais de tecnologia.