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)