DEV Community

Cover image for Criando uma API SQL para Twitter com Typescript - Parte 3: Criando os controllers. Bônus: cors, helmet e os primeiros testes.
Augusto Silva
Augusto Silva

Posted on • Edited on

Criando uma API SQL para Twitter com Typescript - Parte 3: Criando os controllers. Bônus: cors, helmet e os primeiros testes.

Na Parte 2: Configurando as Rotas do Express eu mostrei como montarmos as rotas de 3 maneiras diferentes. E hoje veremos como adicionar os nossos controllers, com o bônus de adicionarmos protecões nas nossas rotas com os pacotes cors, helmet e também os nossos primeiros testes com mocha e chai.

Criando nossa interface controller.

Aqui, para continuar a nossa prática de POO que o typescript fornece, vamos criar uma interface que servirá de base para os nossos controllers. Logo criamos so diretório /controllers e dentro dela criaremos o arquivo controller-interface.ts:

import { Request, Response } from 'express'

export interface Controller {

  get (req: Request, res: Response): void
  post (req: Request, res: Response): void
  put (req: Request, res: Response): void
  delete (req: Request, res: Response): void

}
Enter fullscreen mode Exit fullscreen mode

Aqui importamos os tipos Request e Response do express para que possamos passa-los nas nossas rotas. Delcaramos nessa interface que todo controller deve ter as funções get, post, put e delete que representam os respectivos métodos HTTP de mesmo nome.

Criando nosso primeiro controller: QueryController.

Agora que temos uma base para os nosso futuros controllers, vamos criar o controller que ficará responsável pela rota que receberá as queries em SQL, para isso criaremos o arquivo query-controller.ts dentro da pasta /controllers:

import { Request, Response } from 'express'
import { Controller } from "./controller-interface";

export class QueryController implements Controller {

  constructor() {

  }

  public get (req: Request, res: Response): void {
    //TODO: Implementar a lógica de realizar queries através do método GET, com passagem dos parâmetros na URL.
    res.status(200).send({
      message: "Rota \'/api/v1/query/\' recebeu o método GET e respondeu com uma função do QueryController."
    })
  }

  public post (req: Request, res: Response): void {
    //TODO: Implementar a lógica de realizar queries através do método POST, com passagem dos parâmetros no body em um JSON.
    res.status(201).send({
      message: "Rota \'/api/v1/query/\' recebeu o método POST e respondeu com uma função do QueryController"
    })
  }

  public put (req: Request, res: Response): void {
    res.status(405).send({
      message: "Rota \'/api/v1/query/\' recebeu o método PUT, mas este método não é permitido."
    })
  }

  public delete (req: Request, res: Response): void {
    res.status(405).send({
      message: "Rota \'/api/v1/query/\' recebeu o método DELETE, mas este método não é permitido."
    })
  }

}
Enter fullscreen mode Exit fullscreen mode

Como o nosso QueryController implementa a interface Controller, precisamos que ele implemente os quatros métodos da interface. Como nosso projeto, até agora, só usará os métodos GET e POST, as funções put e delete irá retornar o status 405 identificando que na nossa api os méstodos PUT e DELETE não são permitidos e retornam um json que contem o campo message com a mensagem que tem a mesma informação.

Alterando o arquivo de rotas das queries.

Agora iremos modificar o arquivo responsável pelas rotas das queries, /routes/query-routes.ts, para usar as funções do nosso QueryController:

import { Router } from 'express'
import { Route, BaseRoutes } from './base-routes'
import { QueryController } from '../controllers/query-controller'

export class QueryRoutes extends BaseRoutes {

  private controller: QueryController = new QueryController()

  /**
   * Variável que receberá o Router do express.
   */
  private router: Router = Router()

  /**
   * Array contendo as rotas com o path, tipo, middlewares e funções.
   * 
   * Declaramos aqui todas as nossas rotas referente ao path '/query'.
   */
  private routes: Array<Route> = [
    {
      path: "/query",
      type: "get",
      middlewares: [],
      controllerFunction: this.controller.get
    },
    {
      path: "/query",
      type: "post",
      middlewares: [],
      controllerFunction: this.controller.post
    },
    {
      path: "/query",
      type: "put",
      middlewares: [],
      controllerFunction: this.controller.put
    },
    {
      path: "/query",
      type: "delete",
      middlewares: [],
      controllerFunction: this.controller.delete
    },
  ]

  constructor(basePath: string) {
    super(basePath)
  }

  /**
   * Seta as rotas e as coloca no Router do express.
   * 
   * @param router Router da aplicação Express para adicionar rotas nele.
   * @returns uma flag que indica se o setup aconteceu 'true', ou não aconteceu 'false'.
   */
  public setup (router: Router): boolean {
    this.router = router
    const routes: Array<Route> = this.routes

    if (routes.length === 0) {
      return false
    }

    routes.forEach((element: Route) => {
      if (element.type === "get") {
        router.route(element.path).get(element.middlewares, element.controllerFunction)
      }

      if (element.type === "post") {
        router.route(element.path).post(element.middlewares, element.controllerFunction)
      }

      if (element.type === "put") {
        router.route(element.path).put(element.middlewares, element.controllerFunction)
      }

      if (element.type === "delete") {
        router.route(element.path).delete(element.middlewares, element.controllerFunction)
      }

    });

    return true
  }

  /**
   * Método getter da variável router.
   * 
   * @returns o objeto Router do Express relacionado às rotas da classe.
   */
  public getRouter (): Router {
    return this.router
  }

  /**
   * Método getter da variável router.
   * 
   * @returns o objeto Router do Express relacionado às rotas da classe.
   */
  public getRoutes (): Array<Route> {
    return this.routes
  }

  /**
   * Método setter para a variável basePath.
   * 
   * Deve ser utilizado antes de setar as rotas na inicialização da aplicação.
   * 
   * @param newRoutes array que contém as novas rotas a serem utilizadas.
   */
  public setRoutes (newRoutes: Array<Route>) {
    return this.routes = newRoutes
  }

}
Enter fullscreen mode Exit fullscreen mode

Como já haviamos deixado tudo pronto, a única coisa que fizemos foi modificar a variável routes do tipo Array de Route. Bem direto e simples não é mesmo? E com isso terminamos de adicionar nosso primeiro controller à nossa aplicação.

No próximo artigo estarei adicionando a conexão com a API do Twitter e também já realizaremos nossa primeira query em SQL para consumir a API do Twitter.

Os bônus!

Bônus 01: Adicionando mais segurança à nossa aplicação express.

Como boa prática de segurança, na documentação do próprio express, eles recomendam o uso de dois pacotes: cors e helmet.

CORS - Configurando o CORS de maneira fácil no express!

Com o pacote cors, fica fácil de configurar o CORS de nossa aplicação. Para isso, vamos instalar a dependência:

npm install --save cors
npm install --save--dev @types/cors
Enter fullscreen mode Exit fullscreen mode

E depois adicionamos as seguintes linhas no nosso arquivo index.ts na raíz do projeto:

//...
import cors from 'cors'
//...
app.use(cors())
//...
Enter fullscreen mode Exit fullscreen mode

Para saber mais em como configurar de maneira mais avançada o cors basta acessar a documentação!

Helmet - Protegendo o cabeçalho da sua aplicação!

O helmet é o pacote que nos ajuda a proteger a nossa aplicação de alguma vulnerabildiades. Essa segurança é aplicada no cabeçalho da nossa aplicação.

Ele é um conjunto de nove funções middlewares e você pode saber mais (acessando aqui a documentação)[https://expressjs.com/pt-br/advanced/best-practice-security.html]! Para instalar basta usar o comando:

npm install --save helmet
Enter fullscreen mode Exit fullscreen mode

E depois adicionamos as seguintes linhas no nosso arquivo index.ts na raíz do projeto:

//...
import helmet from 'helmet'
//...
app.use(helment())
//...
Enter fullscreen mode Exit fullscreen mode

Bônus 02: Adicionando os primeiros tests!

Lembra que na parte 01 nos testamos nossa aplicação ao acessar o navegador, na parte 02 eu nem mencionei como testar, mas você poderia ter realizado requisições usando o Insomnia ou Postman. Mas para deixar nossa aplicação mais robusta, iremos adicionar nessa primeira parte os testes unitários!

Para isso vamos adicionar o mocha e o chai, e os tipos de ambos né?! Mas em breve explicação: o mocha é um framework de testes javascript voltado para Node.js e o chai é uma biblioteca, ou lib para os íntimos, de asserções para javascript no Node.js ou no navegador.

E vamos instala-los agora:

npm install --save-dev mocha chai chai-http @types/chai @types/mocha
Enter fullscreen mode Exit fullscreen mode

Detalhe no chai-http que eu não comentei né? Então é uma adicional ao chai para realizar asserções em requisições http, bacana né?

Agora vamos escrever nosso primeiro teste, mas antes vamos criar uma pasta chamada /test e dentro dela o arquivo query.test.ts (o nome do arquivo pode ser qualquer um, contanto que seja .ts também ok?)

Dentro do nosso arquivo vamos escrever o seguinte código:

// Durante os testes, a variavel 'env' é definida como 'test'.
process.env.NODE_ENV = 'test'

import chai from 'chai'
import chaiHttp from 'chai-http'
import server from '../index'

const should = chai.should()

chai.use(chaiHttp)

describe('Queries', () => {

  /**
   * Testa a rota '/query' com o método GET.
   */
  describe('[GET]/route', () => {
    it('Deve retornar o STATUS 200 e um JSON contendo o campo mensagem.', (done) => {
      chai.request(server)
        .get('/api/v1/query')
        .end((err, res) => {
          res.should.have.status(200)
          res.body.should.be.a('object')
          res.body.message.should.be.a('string')
          done()
        })
    })
  })

  /**
   * Testa a rota '/query' com o método POST.
   */
  describe('[POST]/route', () => {
    it('Deve retornar o STATUS 201 e um JSON contendo o campo mensagem.', (done) => {
      chai.request(server)
        .post('/api/v1/query')
        .end((err, res) => {
          res.should.have.status(201)
          res.body.should.be.a('object')
          res.body.message.should.be.a('string')
          done()
        })
    })
  })

  /**
   * Testa a rota '/query' com o método PUT.
   */
  describe('[PUT]/route', () => {
    it('Deve retornar o STATUS 405 e um JSON contendo o campo mensagem.', (done) => {
      chai.request(server)
        .put('/api/v1/query')
        .end((err, res) => {
          res.should.have.status(405)
          res.body.should.be.a('object')
          res.body.message.should.be.a('string')
          done()
        })
    })
  })

  /**
   * Testa a rota '/query' com o método DELETE.
   */
  describe('[DELETE]/route', () => {
    it('Deve retornar o STATUS 405 e um JSON contendo o campo mensagem.', (done) => {
      chai.request(server)
        .delete('/api/v1/query')
        .end((err, res) => {
          res.should.have.status(405)
          res.body.should.be.a('object')
          res.body.message.should.be.a('string')
          done()
        })
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

O arquivo de teste é auto-explicativo e bem direto, se você tiver o inglês arranhando aí, mas se não tiver vou explicar aqui o cada um faz:

  • describe(): é uma função que descreve (e loga no terminal) o teste descrito (quem diria né?!) e recebe uma função de callback;
  • it(): é uma função parecida com o describe(), mas nela de fato será executado os testes assertidos por nós;
  • chai.request(): é uma função que recebe o nosso arquivo do servidor e então realiza requisições nele;
  • .get()/.post()/.put()/.delete(): são funções que definem o método HTTP e recebem como parâmetro a rota da requisição, até parece com o nosso controller!
  • .end(): função que finaliza a cadeia e irá executar as asserções

Para saber mais sobre as asserções, recomendo ler a documentação do chai.

Agora nós vamos alterar o nosso index.ts para funcionar como um módulo e poder ser importado no arquivo de teste, basta adicionar a seguinte linha no final do arquivo:

//[...]
export default app
Enter fullscreen mode Exit fullscreen mode

E por final, vamos alterar o nosso script de test no arquivo package.json:

  "scripts": {
    "test": "mocha -r ts-node/register test/**/*.ts",
    "start": "node index.js",
    "dev": "nodemon index.ts",
    "build": "tsc --project ./"
  }
Enter fullscreen mode Exit fullscreen mode

O nosso script de test invoca o mocha com a flag -r para ele ser de maneira recursiva; o arqugmento ts-node/register serve para o nosso compilador de ts executar antes dos testes e o chai funcionar com o ES6; por fim o argumento test/**/*.ts serve para indicarmos a pasta de test, o nível que ele deve acesssar e a extensão dos arquivos.

Agora para executar os testes basta rodar:

npm test
Enter fullscreen mode Exit fullscreen mode

Se tudo deu certo, no seu terminal deve ter algo assim:

Servidor rodando em http://localhost:3000


  Queries
    [GET]/route
       Deve retornar o STATUS 200 e um JSON contendo o campo mensagem.
    [POST]/route
       Deve retornar o STATUS 201 e um JSON contendo o campo mensagem.
    [PUT]/route
       Deve retornar o STATUS 405 e um JSON contendo o campo mensagem.
    [DELETE]/route
       Deve retornar o STATUS 405 e um JSON contendo o campo mensagem.


  4 passing (48ms)
Enter fullscreen mode Exit fullscreen mode

E por hoje é só. Lembrando que você pode acompanhar o projeto através do repositório no github aqui!

Top comments (0)