Criando uma API REST com Node.js

Conexão com DB, testes unitários, mock de banco de dados e mock de request

Publicado por Lohan Bodevan 3 de Setembro de 2016 às 13:34

Introdução

Do inglês "Application Programming Interface" as APIs são cada vez mais importantes para interligar nossos softwares. Seja porque você está utilizando a arquitetura de micro serviços ou porque precisa expor dados para um cliente externo ou para um APP mobile. 

Neste post vou falar sobre como eu costumo organizar a estrutura de uma API REST com Node.js. Vamos ver como começar o desenvolvimento, configurar conexão com banco de dados, criar testes unitários, mocks e tudo que um software de qualidade pede.

 

O que você deve saber antes de começar?

Estou considerando que você, caro leitor, tenha conhecimentos básicos sobre os itens abaixo:

  • Uso do terminal do seu sistema operacional;
  • JavaScript;
  • Comportamento padrão de uma API REST.

 

Preparando o ambiente

Para este tutorial, precisaremos dos seguites pacotes:

 

Ação

Antes de colocarmos a mão na massa, o que vamos ver aqui neste post está no Github para que você possa consultar enquanto lê. Encontre-o aqui.

Com o Node.js 6.x instalado, significa que você ganhou de brinde o NPM, gerenciador de pacotes que utilizaremos para instalar nossas dependências dentre outras rotinas como, executar testes e iniciar nossa API.

No direitório que você escolheu para desenvolver esta API, inicie as configurações.

npm init

 

Arquitetura da API

A API será dividia em 3 camadas: handlerslibraries 
repositories. Vamos falar mais sobre cada uma delas abaixo.

Handlers

Os handlers são resposáveis por receber as requisições HTTP de nossa API, repassar para a camada de negócio e responder adequadamente ao client que a consome. Fazendo uma analogia com outra arquitetura, nossos handlers seriam o "C" do MVC.

Libraries

Na camada de libraries ou simplesmente lib, nós colocamos bibliotecas que irão fazer com que nossa aplicação se conecte com algum outro serviço, tais como banco de dados, cache, servidores de e-mail e coisas do tipo.

Repositories

Os repositories represantam os dados, ou seja, o que realmente importa na nossa aplicação. Há quem goste de colocar nessa camada também a abstração do banco de dados, ou seja, o model. Particularmente, eu não gosto de misturar a manipulação dos dados com a modelagem do bancoPor isso, adiciono uma subcamada ao repository, chamada model.

Em resumo, a estrutura da nossa pasta de códigos fonte (src) ficaria assim:

Estrutura de arquivos

Estrutura do diretório src

 

Estrutura de arquivos

Estrutura do diretório src/repository

Dependências

Tenho gostado bastante de trabalhar com o Koa, um micro framework criado por devs que participaram do desenvolvimento do Express, porém mais focado em APIs. Ele possui um esquema de middleware onde podemos "plugar" apenas aquilo que iremos utilizar, sem sobrecarregar nossa aplicação.

Utilizaremos os seguintes modulos do Koa: koa-body-parser e koa-router. 

Instalamos ele com o NPM, assim como manda a boa prática:

npm install koa --save
npm install koa-body-parser --save
npm install koa-router --save

Pra quem acompanha o mundo do JavaScript, sabe que houveram muitos avanços na linguagem nos últimos anos que ainda não são suportados pelos browsers e quase todos já são pelo V8. Para garantir total compatibilidade com esses incríveis novos recursos uso o Babel, um compilador que permite "Usarmos hoje a nova geração JavaScript", como promete o slogan.

npm install babel-cli --save
npm install babel-plugin-transform-runtime --save
npm install babel-preset-es2015 --save
npm install babel-register --save-dev

ORM

Para representar o banco de dados, o aclamado Sequelize. E o banco SQLite3, apenas por questões didáticas. O Sequelize trabalha também com PostgreSQL, MySQL entre outros.

npm install sequelize --save
npm install sqlite3 --save

Como padrão em aplicações Node.js, nós temos um arquivo chamado server.js no diretório src:

'use strict'; 

if (process.env.NODE_ENV === 'development') {
require('babel-register');
}

var port = process.env.PORT || 3000;
var app = require('./app');

app.listen(port);

Aqui nenhuma novidade, estamos configurando nossa aplicação para responder a uma porta que caso não for configurada como variavel de ambiente responderá na porta 3000.

Ainda no diretório src você verá um arquivo chamado routes.js. Como o nome sugere, ele irá descrever as rotas da api.

import Router from 'koa-router';

import healthcheck from './handler/healthcheck';
import {getPersons} from './handler/person';

export default function () {
const routes = new Router({});

routes.get('healthcheck', '/healthcheck', healthcheck);
routes.get('persons', '/persons', getPersons);

return routes;
};

Ou seja, ele irá dizer qual handler irá cuidar de uma determinada requisição. No detalhe abaixo, nós dizemos para o nosso objeto routes que as requisições GET feitas para o resource /persons, vão ser processadas pela function getPersons presente no handler person.

routes.get('persons', '/persons', getPersons);

Para ficar claro, imagine que fossemos implementar o cadastro de pessoa. Segundo o padrão REST precisariamos declarar essa rota da seguinte maneira:

routes.post('persons', '/persons', createPerson);

Sendo assim, precisariamos incluir também a function createPerson na lista de imports:

import {getPersons, createPerson} from './handler/person';

E para fechar o nosso diretório src temo app.js. Este arquivo é o nosso "main", ele diz quais os middlewares nossa aplicação vai utilizar e instancia nosso framework.

import Koa from 'koa';
import bodyParser from 'koa-body-parser';

import Router from './routes';
import database from './lib/database';

const app = new Koa();

app
.use(database)
.use(bodyParser())
.use(Router().routes())
.use(Router().allowedMethods());

module.exports = app;

Repository

Vamos olhar mais de perto como ficou o repository Person.

import PersonModel from'./model/person';

class Person {
constructor(connection) {
this._conn= connection;
}

*getAll() {
let model = yield this._getModel();

return (yield model.findAll()).map(i=>i.toJSON());
}

_getModel() {
return PersonModel.getModel(this._conn);
}
}

export default Person;

Repare que o construtor recebe a intancia da conexão com o banco de dados por parâmetro. Isto é bom pois significa que posso criar um mock do banco de dados para executar testes no meu repository. Veremos mais sobre isso quando falarmos dos testes.

O método _getModel retorna um objeto para que possamos fazer as queries. A conexão é repassada e daqui pra frente trabalharemos apenas com o model e não mais com a conexão.

_getModel() {
return PersonModel.getModel(this._conn);
}
return (yield model.findAll()).map(i => i.toJSON());

Caso você não esteja familiarizado com arrow functions recomendo que leia este link. Resumidamente, o método findAll() presente no objeto model retorna uma lista, com o map percorremos cada row do resultado da query, fazendo parse para json. Bem mais simples, não?!

Handlers

Nos handlers nada de especial. Instanciamos o repository, passando a conexão por parâmetro, conforme demonstrado acima e chamamos o método que desejamos neste caso o getAll().

import Person from '../repository/person';

export function *getPersons() {
const person = new Person(this.repository);
this.body = yield person.getAll();
console.info('Found ' + this.body.length + ' persons');
};

Lib

Você deve estar se perguntando de onde saiu a variável this.repository. Ela é um singleton da conexão com o banco de dados, setada no arquivo src/lib/database.js.

import Sequelize from 'sequelize';

let dbName = process.env.DB_NAME || '';
let dbUser = process.env.DB_USER || '';
let dbPassword = process.env.DB_PASSWORD || '';
let dbHost = process.env.DB_HOST ||'';
let dbDialect = process.env.DB_DIALECT || 'sqlite';
let dbStorage = process.env.DB_STORAGE || './database.sqlite';

const sequelize = new Sequelize(dbName, dbUser, dbPassword, {
host: dbHost,
dialect: dbDialect,
storage: dbStorage,
logging: null
});

export default function *(next) {
this.repository = sequelize;
yield next;
};

Então no app.js definimos a lib database.js como um middleware.

app.use(database)

Veja também que no arquivo src/lib/database.js é configurada a conexão com banco de dados. Caso quisessemos utilizar o MySql bastaria mudar o dialect e setar os dados de conexão.

let dbDialect = process.env.DB_DIALECT || 'mysql';

Outro ponto importante é sempre manter informações que, ao serem modificadas não alteram a logica da aplicação, armazenadas como variavel de ambiente. Se decidirmos trocar a senha do banco de dados, por exemplo, não deveria ser necessário fazer um deploy para isso. Recomenda-se então que esse tipo de informação não seja versionada. Assim, a aplicação em cada ambiente (local, homologação e produção) será independente e não correrá o risco da APP em homologação conectar no banco de dados de produção, por exemplo.

Testes

Supertest

Para fazer mock de requisições HTTP, utilizo o Supertest

npm install supertest --save-dev

Para utilizarmos ele é preciso configurar o agent, e podemos fazê-lo da seguinte maneira.

import Supertest from 'supertest';
import app from '../src/app';
let request = Supertest.agent(app.listen());

Com esta configuração modemos fazer mock de nossas requisições e testar as respostas.

it('Healthcheck should return 200 and json content type', function(done) {
        let expected = {
            service: true
        }

        request
            .get('/healthcheck')
            .set('Accept', 'application/json')
            .expect('Content-Type', /json/)
            .expect(HTTPStatus.OK)
            .end(function(err, res) {
                if (err) return done(err);

                assert.equal(HTTPStatus.OK, res.status);
                assert.equal(expected.service, res.body.service);
                done();
            });
    });

Legal né!? Veja que o teste aguarda que o status code do response seja 200 (HTTPStatus.OK). Caso meu handler retorne qualquer coisa diferente, o teste irá falhar. 

Testando Banco de Dados

O título é para causar, pois de fato o que não queremos é testar o banco de dados. Não queremos testar o SGDB. Nossos testes unitários devem testar o que eu faço com a resposta desse ou qualquer outro serviço, por isso nós fazemos um mock. Se realmente quisermos testar o SGDB, ou seja, a integração da nossa aplicação com ele, deveríamos criar então Testes de Integração. Mas, isso é um assunto que vou deixar para outro post.

t('getAll method should return collection', function *getAll(done) {
        const person = new Person(MockRepository);
        let collection = yield person.getAll();

        assert.isArray(collection);
        assert.equal(collection.length, 2);

        expect(collection[0]).to.have.property("id");
        expect(collection[0]).to.have.property("firstName");
        expect(collection[0]).to.have.property("lastName");
        expect(collection[0]).to.have.property("createdAt");
        expect(collection[0]).to.have.property("updatedAt");

        done();
    });

Este Test Case, está verificando que, ao chamar o método getAll() do repository person ele me retorna uma collection e esta collection deve conter algumas propriedades. Ou seja, não estamos testando a query, isto não é importante neste momento. Queremos saber se o método, dado um resultado de query, trabalhará da forma que esperamos.

Lembra que nossa classe Person recebia uma conexão como parâmetro no método construtor? Então, é aí que iremos injetar nosso mock.

const person = new Person(MockRepository);

Sequelize oferece interface para fazermos mock e recomendo usá-la. Porém eu fiz "na mão" para ficar mais claro o conceito.

class ModelMock {
    create(options) {
        return {}; 
    }

    sync() {
        return new Promise(
            function(resolve, reject) {
                setTimeout(function() {
                    resolve()
                }, 1000)
            }  
        );
    }

    *findAll() {
        return [new PersonMock(), new PersonMock()];
    }
};

class PersonMock {
    toJSON() {
        return {
            id: 1,
            firstName: "John",
            lastName: "Hancock",
            createdAt: "2016-07-26T19:59:25.253Z",
            updatedAt: "-07-26T19:59:25.253Z"
        }
    }
};

class MockRepository {
    static define(name, options) {
        return new ModelMock();
    }
};

Resumidamente, MockRepository simula nossa conexão com o banco, ModelMock irá simular o método findAll do ORM e o PersonMock é o nosso resultado da query. Um ponto negativo de fazer o mock dessa forma é que se eu mudar o ORM, preciso reescrever os mocks.

Dica! Para testar funções assíncronas, existe uma lib muito bacana chamada Mocha que facilita esse trabalho.

npm install mocha --save-dev
npm install co-mocha --save-dev

Com ela você também consegue configurar o Babel, assim seus testes também poderão ser escrito utilizando padrões ECMAScript. Para tal, crie um arquivo chamado mocha.opts na raiz do diretório tests com as seguintes opções.

--require babel-register
--require co-mocha
--timeout 5000

 

Resultado

Isso é a base para a partir daqui comecar o desenvolvimento de uma API flexível, com testes e camadas bem definidas. Claro que existes muitos pontos que gostaria de detalhar aqui porém, para não ficar ainda mais extenso, vou deixar para próximos posts. Este repositório no Github está aberto, e aceito contribuições.

 

Conclusão

O que apresentei aqui não está escrito em pedra, esta é a forma como eu gosto de organizar minha APP quando vou criar uma API com Node.js. É importe dizer que se fosse fazer a mesma API em Python a organização seria diferente, porque, ainda que tenhamos nossas preferências, cada linguagem possui seu estilo de programação. Temos que tomar cuidado para não programar "Java no Python", ou "PHP no Node.js" e etc. É preciso respeitar o estilo de cada linguagem.

E você? como organiza sua API? Deixe seu comentário abaixo. 

Me diga o que achou sobre esse estilo de post e como posso melhorar.

Bom estudo!


Comentários