API Node Desacoplada: Criando Cadastro de Usuários

zorasantos

Zoranildo Santos

Posted on July 28, 2023

API Node Desacoplada: Criando Cadastro de Usuários

Neste artigo vamos dar continuidade a nossa série Guia Prático: Aprenda a Construir uma API Node.js Desacoplada e criar um simples endpoint para cadastro de usuários. Por mais que seja simples vamos abordar métodos essenciais para termos um código limpo e desacoplado. Lembrando que nesse artigo vamos criar essa feature como normalmente vemos em alguns cursos e vídeos no youtube ferindo os princios S.O.L.I.D. No próximo artigo aí sim, vamos refatorar essa feature seguindo os princípios do S.O.L.I.D, com isso você terá uma ideia do que muda no código e porque.

Definindo a estrutura de pastas

A estrutura de pastas usada nesse projeto é uma escolha pessoal, você pode usar outra estutura, o importante é entender os conceitos e metodologias utilizadas.

Vamos criar dentro de src uma pasta chamada application. A ideia é de ser possível mudar qualquer framework ou ferramenta da infraestrutura sem mudar uma linha sequer no código da nossa aplicação.

Dentro de application vamos criar uma pasta chamada modules e dentro de modules vamos criar pastas de acordo com as nossas features que por sua vez terá uma estrutura particular.

A feature que vamos desenvolver agora é um endpoint para cadastrar usuários, então dentro da pasta modules vamos criar outra chamada SignUp. Dentro da pasta SignUp vamos criar mais três pastas, entitie, repository, useCase.

Ao final deveremos ter a seguinte estrutura de pastas:

estrutura de pastas

Criando a entidade

Dentro da pasta entitie crie o arquivo User.ts e insira o código abaixo:

Escrevemos comentários dentro do código explicando detalhadamente cada linha.

import { randomUUID } from 'node:crypto'

// Importa a função do randomUUID do pacote Node.js crypto para
// gerar um identificador único para cada objeto User.

export interface IUserProps {
  name: string
  password: string;
}

// Essa interface descreve a estrutura esperada para as
// propriedades do usuário. Ela define que o objeto user deve ter
// duas propriedades: name (que deve ser uma string) e password
// (que também deve ser uma string).

export class User {
// A classe User é declarada e contém as propriedades e métodos
// relacionados ao usuário.

  private _id: string
  private _createdAt: Date
  private _updatedAt: Date
  private props: IUserProps

// A classe possui três propriedades privadas: _id, _createdAt e
// _updatedAt, que armazenam o identificador único, a data de
// criação e a data de atualização, respectivamente.

// Além das propriedades privadas, também há uma propriedade
// props, que armazena um objeto do tipo IUserProps. Essa
// propriedade é usada para armazenar os dados de nome (name) e
// senha (password) do usuário.

  constructor(props: IUserProps) {
    this._id = randomUUID()
    this._createdAt = new Date()
    this._updatedAt = new Date()
    this.props = props
  }

// O construtor da classe recebe um objeto do tipo IUserProps
// como argumento. Ele é responsável por inicializar as
// propriedades _id, _createdAt e _updatedAt, além de armazenar as
// propriedades name e password passadas no objeto props.

  public get id() {
    return this._id
  }

  public set name(name: string) {
    this.props.name = name
  }

  public get name(): string {
    return this.props.name
  }

  public set password(password: string) {
    this.props.password = password
  }

  public get password(): string {
    return this.props.password
  }

  public get createdAt(): Date {
    return this._createdAt
  }

  public get updatedAt(): Date {
    return this._updatedAt
  }

// A classe define os getters e setters para permitir o acesso
// controlado às propriedades.

// Os getters são métodos que permitem obter o valor das
// propriedades, enquanto os setters são métodos que permitem
// definir os valores das propriedades.

// O uso de getters e setters é uma forma de encapsular as
// propriedades da classe e controlar como elas são acessadas e
// modificadas. Isso é útil para garantir a integridade dos dados
// e aplicar lógicas adicionais ao acessar ou definir essas
// propriedades.

}

Enter fullscreen mode Exit fullscreen mode

Criando o Repository

Dentro da pasta repository crie o arquivo UserRepository.ts e insira o código abaixo:

import { User } from '../entitie/User'

export class UserRepository {
  public users: User[] = []

  async create(data: User): Promise<void> {
    this.users.push(data)
  }
}


Enter fullscreen mode Exit fullscreen mode

Criando o useCase

Dentro da pasta useCases vamos criar uma nova pasta que levará o nome do nosso caso de uso que é o cadastro de um novo usuário, ou seja, crie uma pasta chamada CreateSignUp.

Dentro da pasta CreateSignUp crie o arquivo CreateSignUpUseCase.ts e insira o código abaixo:

import { User } from '../../entitie/User'
import { UserRepository } from '../../repository/UserRepository'

interface IRequest {
  name: string
  password: string
}

export class CreateSignUpUseCase {
  constructor(private repository: UserRepository) {}

  async execute(request: IRequest): Promise<void> {
    const { name, password } = request

    const user = new User({
      name,
      password,
    })

    await this.repository.create(user)
  }
}

Enter fullscreen mode Exit fullscreen mode

Criando o Controller

Crie dentro da pasta CreateSignUp o arquivo CreateSignUpController.ts e insira o código abaixo:

import { Response, Request } from "express"
import { CreateSignUpUseCase } from './CreateSignUpUseCase'

interface ICreateSignUpDTO {
  name: string
  password: string
}

export class CreateSignUpController {
  constructor(private readonly useCase: CreateSignUpUseCase) {}

  async handle(req: Request, res: Response): Promise<Response> {
    const { name, password } = req.body as ICreateSignUpDTO

    const data = { name, password }
    await this.useCase.execute(data)

    return res.status(201).send({ message: 'User created successfully' })
  }
}

Enter fullscreen mode Exit fullscreen mode

Criando o Factory

Crie dentro da pasta CreateSignUp o arquivo CreateSignUpFactory.ts e insira o código abaixo:

import { UserRepository } from '../../repository/UserRepository'
import { CreateSignUpController } from './CreateSignUpController'
import { CreateSignUpUseCase } from './CreateSignUpUseCase'

export const CreateSignUpFactory = (): CreateSignUpController => {
  const repository = new UserRepository()
  const useCase = new CreateSignUpUseCase(repository)
  const controller = new CreateSignUpController(useCase)

  return controller
}


Enter fullscreen mode Exit fullscreen mode

Criando rotas com Express

Aqui nosso projeto vai ferir mais alguns princípios, mas tudo bem, vamos corrigir isso nos próximos artigos.

Dentro da pasta infra crie uma nova pasta chamada routes e dentro crie dois arquivo signUp.routes.ts e index.ts.

Insira no arquivo signUp.routes.ts o código abaixo:

import { Router } from 'express'
import { CreateSignUpFactory } from '../../application/modules/SignUp/useCases/CreateSignUp/CreateSignUpFactory'
const signupRoutes = Router()

signupRoutes.post('/signup', (req, res) =>  CreateSignUpFactory().handle(req, res))

export { signupRoutes }

Enter fullscreen mode Exit fullscreen mode

Insira no arquivo index.ts o código abaixo:

import { Router } from 'express'

import { signupRoutes } from './signUp.routes'


const router = Router()

router.use('/v1', signupRoutes)

export { router }

Enter fullscreen mode Exit fullscreen mode

Feito isso é necessário registrar as rotas no server do express. Atualize o arquivo index.ts(src/infra/ports/express/index.ts) do express com o código abaixo :

import 'dotenv/config'
import express from 'express'
import { router } from '../../routes'

const PORT = 5000

const app = express()

// código novo 
app.use(express.json())
app.use(router)
// 

app.listen(PORT, () => {
  console.log(`Express app listening on port ${PORT}`)
})

export { app }
Enter fullscreen mode Exit fullscreen mode

app.use(express.json())
O middleware express.json() é um middleware embutido no Express que é usado para analisar o corpo das requisições HTTP com o formato JSON. Ele verifica o corpo da requisição e, se encontrar dados JSON válidos, os converte em um objeto JavaScript, que é então acessível através do objeto req.body.

Quando você usa app.use(express.json()), você está adicionando esse middleware à instância do servidor, o que significa que todas as rotas que estão definidas após esta chamada terão acesso aos dados JSON enviados no corpo das requisições e poderão acessá-los através do req.body.

app.use(router)
Nesta linha, estamos usando outro middleware, mas em vez de usar um middleware embutido como express.json(), estamos usando um middleware personalizado chamado router. Esse router é uma instância do express.Router() que foi definida nos arquivos dentro da pasta routes para organizar as rotas da nossa aplicação.

O express.Router() permite agrupar as rotas relacionadas em um único objeto, tornando mais fácil definir e organizar as rotas em diferentes arquivos, o que ajuda a manter o código limpo e modularizado.

Testando a requisição

Muito bem, agora é hora de testar a aplicação desenvolvida até aqui. Use alguma ferramenta como insomnia ou postman para testar a requisição.

Primeiro confira se o valor da variável SERVER_TYPE no arquivo .env é express e suba a aplicação rodando o comando yarn start:dev no terminal. A url pra acessar é essa: http://localhost:5000/v1/signup. Se tudo estiver certo até aqui deveremos ter esse resultado:

teste request

Se colocarmos um console.log no repository veremos o objeto que foi criado e salvo na memória, então altere o arquivo UserRepository.ts inserindo um console.log:

...
  async create(data: User): Promise<void> {
    this.users.push(data)
    console.log("Objeto salvo na memória", this.users)
  }
...
Enter fullscreen mode Exit fullscreen mode

objeto salvo na memória

Agora faça o seguinte teste:

  1. Pare a aplicação:
  2. Mude o valor da variável SERVER_TYPE para fastify.
  3. Inicialize a aplicação novamente rodando o comando yarn start:dev no terminal.

log de erro

Com isso nosso app não vai conseguir criar um novo cadastro de usuário pois não vai encontrar a rota definida, tendo em vista que essa rota só existe no express. Para o nosso app rodar sem problemas tanto com express, quanto com o fastfy teremos que criar o roteamento do fastify.

Criando rotas com Fastify

Dentro da pasta routes Crie duas novas pastas express e fastify.

Mova os arquivos index.ts e signUp.routes.ts que estão dentro da pasta routes para a pasta express.

No arquivo signUp.routes.ts altere o path que importa o arquivo CreateSignUpFactory.ts.

// Antes
import { CreateSignUpFactory } from '../../application/modules/SignUp/useCases/CreateSignUp/CreateSignUpFactory'

// Depois
import { CreateSignUpFactory } from '../../../application/modules/SignUp/useCases/CreateSignUp/CreateSignUpFactory'
Enter fullscreen mode Exit fullscreen mode

No arquivo index.ts dentro da pasta express que fica dentro da pasta ports, altere o path que importa o arquivo de rotas.

// Antes
import { router } from '../../routes'

// Depois
import { router } from '../../routes/express'
Enter fullscreen mode Exit fullscreen mode

Antes de continuar confira se a aplicação continua rodando.

Depois de feito a refatoração acima, finalmente vamos criar a rota. Dentro da pasta fastify que fica dentro de routes crie o arquivo index.ts e insira o seguinte código:

import { FastifyPluginAsync } from 'fastify'
import { CreateSignUpFactory } from '../../../application/modules/SignUp/useCases/CreateSignUp/CreateSignUpFactory'


export const signUpRouter: FastifyPluginAsync = async (
  fastify
): Promise<void> => {
  fastify.post('/signup', (req, res) => CreateSignUpFactory().handle(req, res))
}
Enter fullscreen mode Exit fullscreen mode

A estrutura de pastas até aqui deverá ser essa:

nova estrutura de pastas

Registrar a rota

Depois de criada a rota é necessário registrá-la. No arquivo index.ts dentro da pasta fastify que fica dentro da pasta ports(src/infra/ports/fastify/index.ts) importe o arquivo de rotas e registre na instância do fastify. Com a atualização o arquivo ficará assim:

import 'dotenv/config'
import fastify from 'fastify'
// Código novo: importa o arquivo de rota do fastify
import { signUpRouter } from '../../routes/fastify/index'

const server = fastify({ logger: true })

// Código novo: registra a rota na instância criada
server.register(signUpRouter, { prefix: '/v1' })

const app = async () => {
  try {
    server.listen({
      host: '0.0.0.0',
      port: process.env.PORT_SERVER ? Number(process.env.PORT_SERVER) : 5000,
    })
    console.log(`Server fastify running in port ${5000}`)
  } catch (error) {
    console.error(`Erro server fastify`, error)
  }
}

app()

Enter fullscreen mode Exit fullscreen mode

Isso ainda não é o suficiente pra nossa aplicação funcionar com o fastify. Como já mencionei, nosso controller(CreateSignUpController) está totalmente acoplado ao express e o fastify tem tipagens e métodos diferentes. Mas pra testarmos a aplicação funcionando com o fastify, vamos fazer uma pequena alteração no CreateSignUpController.ts.

No controller vamos remover o import da tipagem do express e importar a tipagem do fastify.

// código para remover
import { Response, Request } from "express"
// Código novo
import { FastifyRequest, FastifyReply } from 'fastify'

import { CreateSignUpUseCase } from './CreateSignUpUseCase'

interface ICreateSignUpDTO {
  name: string
  password: string
}

export class CreateSignUpController {
  constructor(private readonly useCase: CreateSignUpUseCase) {}

// aqui no lugar das tipagens do express(Request, Reponse) colocamos a tipagem do fastify(FastifyRequest, FastifyReply)
  async handle(req: FastifyRequest, res: FastifyReply): Promise<Response> {
    const { name, password } = req.body as ICreateSignUpDTO

    const data = { name, password }
    await this.useCase.execute(data)

    return res.status(201).send({ message: 'User created successfully' })
  }
}

Enter fullscreen mode Exit fullscreen mode

Com isso podemos testar a requisição. Se tudo estiver certo a resposta no insomnia será a mesma do teste anterior com express e o console.log que está no repository terá essa saída no terminal:

print do log

Se você observar bem, depois de deixar a aplicação funcionado com o fastify, o arquivo signUp.routes.ts tem um erro de tipagem. Isso ocorre porque no controller(CreateSignUpController.ts) estamos usando o fastify e no arquivo signUp.routes.ts estamos usando o express. Pra resolver isso precisamos criar adaptadores pra que nossa aplicação consiga funcionar seja com fastify, seja com express sem a necessidade de alterar o controller(CreateSignUpController.ts).

Ficamos por aqui. Apesar da nossa aplicação estar ferindo alguns princípios, temos algo já bem estruturado e modularizado. Até o próximo artigo.

💖 💪 🙅 🚩
zorasantos
Zoranildo Santos

Posted on July 28, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related