Criando uma API SQL para Twitter com Typescript - Parte 3: Criando os controllers. Bônus: cors, helmet e os primeiros testes.
Published: 2021-03-01 23:48:30

Na [Parte 2: Configurando as Rotas do Express](https://dev.to/augustoasilva/criando-uma-api-sql-para-twitter-com-typescript-parte-2-configurando-as-rotas-do-express-3eje) 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`:

```typescript
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

}
```

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`:

```typescript
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."
    })
  }

}
```

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:

```typescript
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
  }

}
```

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
```

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

```typescript
//...
import cors from 'cors'
//...
app.use(cors())
//...
```

Para saber mais em [como configurar de maneira mais avançada o cors](https://expressjs.com/en/resources/middleware/cors.html) 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
```

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

```typescript
//...
import helmet from 'helmet'
//...
app.use(helment())
//...
```

### 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
```

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:

```typescript
// 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()
        })
    })
  })
})
```

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](https://www.chaijs.com/api/).

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:

```typescript
//[...]
export default app
```

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

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

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
```

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

```powershell
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)
```

E por hoje é só. Lembrando que você pode acompanhar o projeto [através do repositório no github aqui](https://github.com/augustoasilva/twit-sql)!