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 }
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." }) } }
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 } }
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:
//... import cors from 'cors' //... app.use(cors()) //...
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
E depois adicionamos as seguintes linhas no nosso arquivo index.ts
na raíz do projeto:
//... 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:
// 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.
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
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 ./" }
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:
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!
Top comments (0)