NodeJS e Express

Introdução

Neste artigo será mostrado como criar um projeto com NodeJS e Express e vamos expor uma API para podermos realizar as quatro operações básicas de um CRUD (criar, ler, atualizar e deletar dados). Também será mostrado como podemos construir um projeto de forma simples, descomplicada, com baixa acoplamento e alta coesão entre seus componentes por meio de injeção de dependências e inversão de controle.

Fazendo uma breve introdução sobre as tecnologias que serão apresentadas nesse artigo, primeiramente temos o NodeJS que é um projeto open-source criado para ser um ambiente de desenvolvimento backend escrito em JavaScript, ele explora os benefícios que o JavaScript possui, como orientação a eventos e assíncronicidade.

Juntamente com o NodeJS usaremos nesse projeto o Express que é um framework para desenvolvimento de aplicações web minimalista, isso significa que ele é bem leve e simples mas que não trás consigo todas as funcionalidades por padrão de um servidor web e isso é uma grande vantagem do Express pois é um dos motivos pelo qual ele é muito flexível, e por meio de middlewares é possível plugar libs e ferramentas que nos ajudam no desenvolvimento.

Criando o projeto

O projeto consistirá em uma agenda de contatos, onde poderemos criar um contato novo, buscar um contato ou todos, editar um existente e deletar um contato.

Existem algumas maneiras de criar um projeto com Express, o próprio Express possui um cli para criação.

Aqui iremos fazer de uma forma que eu considero mais simples que é criar via command line* com **NPM.

Vamos criar uma pasta chamada phonebook e após isso, criar o projeto usando NPM:

mkdir phonebook && cd phonebook
npm init -y

Com isso temos a estrutura básica do projeto que nada mais é do que um arquivo package.json:

{
  "name": "phonebook",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Vamos aproveitar e instalar as dependências que vamos precisar para iniciar esse projeto:

npm install express body-parser

E também as dependências que usaremos posteriormente para subir o nosso server no ambiente de local de desenvolvimento e testes:

npm install --save-dev nodemon jest supertest

Agora falta criarmos o arquivo que será executado quando iniciarmos a aplicação, vamos chamá-lo de index.js:

const express = require('express')
const app = express()
const bodyParser = require('body-parser')

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))

app.use((req, resp, next) => {
    resp.set('Access-Control-Allow-Origin', '*')
    next()
})

const server = app.listen(3000, () => console.log('A API está funcionando!'))

module.exports = server

Só com isso podemos executar o node chamando o arquivo index.js que deverá funcionar:

npm run dev

> [email protected] dev /Users/guilherme/develop/repo/phonebook
> nodemon index.js

[nodemon] 2.0.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
A API está funcionando!

Modelando o domínio

Decidi começar pela definição de modelo, porque apesar desse modelo ser simples eu entendo que é sempre bom deixar essa base pronta pois fica mais fácil construir a aplicação em volta de um domínio do que o contrário. Acredito que utilizar outras tecnologias fica mais flexível de mudar do que a mudança no domínio nessa abordagem.

Então iremos criar uma pasta chamada model e nela a index.js:

const Contact = {

    id: 0,
    name: "",
    telephone: "",
    address: ""

}

module.exports = Object.create(Contact)

A definição acima é a representação do que seria um contato em uma agenda de contatos composta pelo id, (aqui entra um discussão sobre Entidades e VO mas para esse exemplo deixei com um id pois acredito que o modelo em um projeto real não deveria possuir um id provavelmente voltaremos a esse ponto em um artigo futuro) name, telephone e address que são Strings e no exports criamos esse objeto com a função create.

Criando a Repository

Após ter criado o domínio vamos criar a nossa Repository que será responsável por lidar com a persistência de dados. Você pode ter percebido que até o momento não adicionamos nenhuma dependência de persistência então como iremos criar o comportamento responsável por isso?

Vamos aqui simplificar um pouco as coisas e iremos criar uma persistência em memória e mais a frente vamos entender como podemos deixar tudo bem simples e desacoplado usando a Injeção de Dependências e Inversão de Controle.

Vamos criar uma pasta chamada repository e dentro dela o nosso arquivo index.js:

class InMemoryRepository{

    constructor(){
        this._data = []
    }

    insert(contact){
        this._data.push(contact)
    }

    selectAll(){
        return this._data
    }

    selectById(id){
        return this._data.find(c => c.id === id)
    }

    update(id, contact){

        const elementId = this._data.findIndex(element => element.id === id);
        contact.id = id

        const updateContact = Object.assign(this._data[elementId], contact)

        this._data[elementId] = updateContact

        return this._data[elementId]
    }

    remove(id){

        const index = this._data.findIndex(element => element.id === id)

        this._data.splice(index, 1)

    }
}

module.exports = InMemoryRepository

Foi usada uma abordagem de classe aqui justamente para depois podermos usar a Injeção de Dependências, mas podemos ver também que temos uma variável membro chamada _data que é um array e temos as funções que irão fazer as nossas operações de CRUD em cima desse array.

Após isso exportamos a nossa classe InMemoryRepository.

Criando Services

Agora chegou a hora de criar a camada da aplicação que é responsável por executar a lógica de negócios.

Vamos criar uma pasta chamada service e dentro dela o arquivo index.js:

class Service{

    constructor(repository){
        this.repository = repository
    }

    create(body){
        this.repository.insert(body)
    }

    getById(id){
        return this.repository.selectById(parseInt(id, 2))
    }

    getAll(){
        return this.repository.selectAll()
    }

    put(id, body){
        return this.repository.update(parseInt(id, 2), body)
    }

    remove(id){
        this.repository.remove(parseInt(id, 2))
    }

}

module.exports = Service

Aqui também usamos a abordagem de classe, mas por que?

Pois assim é possível injetar a dependência do repository no construtor e com isso o controle é invertido já que a Service desconhece qual será a implementação a ser usada, a única coisa que importa para a Service é que a repository que será passada deve possuir as funções de insert, selectById, selectAll, update e remove. Não é responsabilidade da Service saber se a repository é um banco em memória, MongoDB, Postgres ou qualquer outro meio de persistir dados.

Caso seja necessário no futuro implementar alguma outra ação ou mude a lógica de negócios, esta alteração deve ser implementada na Service e caso necessite de outra dependência deve ser adicionado ou injetado no construtor da classe.

Configurando Rotas

Vamos criar as rotas da nossa aplicação, aqui iremos definir quais verbos HTTP iremos deixar disponíveis e que iremos direcionar as requisições quando elas chegarem.

const router = require('express').Router()
const InMemoryRepository = require('../repository')
const Service = require('../service')
const service = new Service(new InMemoryRepository())

router.post('/', (req, res) => {
    const contact = req.body

    service.create(contact)

    res.status(201).json(contact)
})

router.get('/:id', (req, res) => {

    const id = req.params.id

    const result = service.getById(id)
    if(result !== undefined){
        res.status(200).json(result)
        return
    }

    res.sendStatus(204)

})

router.get('/', (req, res) => {

    const result = service.getAll()

    if(result.length > 0){
        res.status(200).json(result)
        return
    }

    res.sendStatus(204)

})

router.put("/:id", (req, res) => {

    const id = req.params.id
    const body = req.body

    const result = service.put(id, body)

    res.status(200).json(result)
})

router.delete("/:id", (req, res) => {

    const id = req.params.id

    service.remove(id)

    res.sendStatus(204)
})

router.get('/health', (req, res) => {

    res.status(200).json({status: "Ok"})
})

router.options('/', (req, res) => {
    res.set('Access-Control-Allow-Methods', 'GET, POST')
    res.set('Access-Control-Allow-Headers', 'Content-Type')
    res.status(204)
    res.end()
})

module.exports = router

Vamos por partes pra entender tudo o que está no código acima:

const router = require('express').Router()
const InMemoryRepository = require('../repository')
const Service = require('../service')
const service = new Service(new InMemoryRepository())

Nesse trecho importamos do próprio Express a dependência do Router que irá disponibilizar aqui os verbos HTTP, importamos aqui as classes InMemoryRepository e Service e em seguida instânciamos a Service e passamos a dependência de uma Repository para ela que nesse caso será a InMemoryRepository.

router.post('/', (req, res) => {
    const contact = req.body

    service.create(contact)

    res.status(201).json(contact)
})

Aqui usamos o router e chamamos o método post e passamos qual será o path ou caminho que será exposta na API, aqui deixamos com '/' para indicar que não queremos passar nada na url só de chamar um POST ele será atendido por esse método.

A função post trás consigo o request e o response e com isso podemos extrair algumas informações importantes no request e adicionar dados no response.

No exemplo acima conseguimos pegar o body que é enviado na requisição e após executar a lógica na Service adicionar o status e o body no response.

Aqui abaixo temos as implementações do GET:

router.get('/:id', (req, res) => {

    const id = req.params.id

    const result = service.getById(id)
    if(result !== undefined){
        res.status(200).json(result)
        return
    }

    res.sendStatus(204)

})

router.get('/', (req, res) => {

    const result = service.getAll()

    if(result.length > 0){
        res.status(200).json(result)
        return
    }

    res.sendStatus(204)

})

O interessante aqui é entender que no request também conseguimos pegar parâmetros passados na url para isso precisamos de um identificador no path que é passado na função get no caso acima é :id a na função pegamos o valor através da sintaxe req.params.id.

A lógica nas requisições GET é que caso não encontre dados na consulta retorne o status 204 - No Content e caso encontre retorna 200 - Ok com os dados solicitados.

Os métodos para PUT e DELETE seguem a mesma lógica.

Configurando o Express e middlewares

Temos a Service e as Rotas configuradas e agora é necessário adicionar o módulo de rotas ao Express para que ele consiga utilizar e assim ficar disponível para ser usado.

No arquivo index.js na raiz do projeto já existe uma configuração:

const express = require('express')
const app = express()
const bodyParser = require('body-parser')

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))

app.use((req, resp, next) => {
    resp.set('Access-Control-Allow-Origin', '*')
    next()
})

const server = app.listen(3000, () => console.log('A API está funcionando!'))

module.exports = server

Com essa configuração já estamos usando os middlewares, onde adicionamos as funções que queremos complementar ao Express, acima estamos usando a lib body-parser para ajudar com o parse da resposta e outro middleware para tratativa de CORS e vamos adicionar o nosso módulo de rotas:

const express = require('express')
const app = express()
const bodyParser = require('body-parser')
const router = require('./router')

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))

app.use((req, resp, next) => {
    resp.set('Access-Control-Allow-Origin', '*')
    next()
})

app.use('/api', router)

const server = app.listen(3000, () => console.log('A API está funcionando!'))

module.exports = server

Acima foi importado o módulo router e adicionado no Express através da função use onde definimos o path raiz da nossa API e no segundo argumento o módulo router.

Iniciando a aplicação

Podemos iniciar a aplicação dessa forma:

nodemon index.js

E fazendo um POST:

curl --location --request POST 'http://localhost:3000/api' \
--header 'Content-Type: application/json' \
--data-raw '{

    "id": 1,
    "name": "Kelly",
    "telephone": "118888888",
    "address": "Rua dos Bobos n 1"

}' | json_pp

Teremos a seguinte resposta:

{
   "id" : 1,
   "name" : "Kelly",
   "address" : "Rua dos Bobos n 1",
   "telephone" : "118888888"
}

Testes

No começo do artigo adicionamos as dependências do jest e supertest e agora vamos implementar um teste.

Na pasta router vamos criar o arquivo router.test.js, seguindo a convenção de nomenclatura do jest para que ele saiba quais arquivos devem ser testados.

Dentro do arquivo vamos criar a nossa primeira suite de testes para testar a rota de POST:

const supertest = require('supertest')
const server = require('../index')

afterAll( async () => {
  server.close()
});

describe('Make requests to the server', () => {

    it('Should create a contact', async () => {
        const resp = await supertest(server).post('/api').send({
            "id": 1,
            "name": "Kelly",
            "telephone": "118888888",
            "address": "Rua dos Bobos n 1"
        });

        expect(resp.statusCode).toEqual(201)
        expect(resp.body.name).toEqual("Kelly")
    })

})

Aqui importamos a lib do supertest e o arquivo index.js da raíz do projeto, primeiramente adicionamos uma função chamada afterAll para que após o testes serem rodados a aplicação seja terminada.

Criamos a suite de testes com a função describe e dentro dela colocamos os testes necessários para testar aquela suite com a função it.

Para fazer o mock da requisição usamos o supertest a passamos para ele o nosso server, invocamos a função HTTP que queremos testar passando o path e com a função send passamos o json que será enviado.

const resp = await supertest(server).post('/api').send({
    "id": 1,
    "name": "Kelly",
    "telephone": "118888888",
    "address": "Rua dos Bobos n 1"
});

Com o retorno do response conseguimos fazer as asserções dos testes, nesse caso queremos testar que a cada POST bem sucedido iremos retornar o status code 201 - Created e o body será devolvido então podemos fazer a asserção de algum campo do response.

expect(resp.statusCode).toEqual(201)
expect(resp.body.name).toEqual("Kelly")

Agora conseguimos rodar o comando a seguir para rodar esse teste:

jest --coverage

E teremos a seguinte resposta:

> jest --coverage --runInBand

 PASS  router/route.test.js
  Make requests to the server
    ✓ Should create a contact (65 ms)

  console.log
    A API está funcionando!

      at Server.<anonymous> (index.js:16:47)

---------------------------|---------|----------|---------|---------|----------------------------------
File                  | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s                
---------------------------|---------|----------|---------|---------|----------------------------------
All files             |   48.68 |        0 |   29.17 |      50 |                                  
 phonebook            |     100 |      100 |     100 |     100 |                                  
  index.js            |     100 |      100 |     100 |     100 |                                  
 phonebook/model      |     100 |      100 |     100 |     100 |                                  
  index.js            |     100 |      100 |     100 |     100 |                                  
 phonebook/repository |      20 |      100 |   22.22 |      25 |                                  
  index.js            |      20 |      100 |   22.22 |      25 | 12-35                            
 phonebook/router     |   39.47 |        0 |   14.29 |   39.47 |                                  
  index.js            |   39.47 |        0 |   14.29 |   39.47 | 16-24,30-37,43-48,53-57,62,66-69 
 phonebook/service    |      50 |      100 |   33.33 |      50 |                                  
  index.js            |      50 |      100 |   33.33 |      50 | 14-26                            
---------------------------|---------|----------|---------|---------|----------------------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.015 s
Ran all test suites.

Além do teste passamos o parâmetro --coverage e com isso é gerado um html com um relatório da cobertura dos testes.

Conclusão

Nesse artigo iniciamos a construção de uma API REST do zero usando NodeJS e Express. Vimos a facilidade de usar o Express e como o mecanismo de middleware torna o desenvolvimento flexível e dinâmico. Também conseguimos ver como deixar uma aplicação desacoplada utilizando o conceito de Injeção de dependências

Código fonte e recursos

Segue o GitHub do projeto e a Collection do Postman

20