From 00a87decca32d5333e8bfe0864e2734c9dd9f29d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jo=C3=A3o=20Victor=20Risso?= <jvtr12@c3sl.ufpr.br>
Date: Wed, 17 Aug 2016 19:50:19 -0300
Subject: [PATCH] Change API to use ECMAScript6
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Major modifications:
- Source code is now in the src directory
- All source files were adapted to ECMAScript6
- Add Gulpfile and Babel to transcompile the project from ES6 to ES5 (until Node.js fully supports ES6)
	- By running gulp one generates the build directory with the files transcompiled, from which the API can be run
- Add ESLint to check for syntax errors and enforce code standards
	- Javascript coding standard currently adopted is AirBnB with 4 spaces
- Replace monetdb package with monetdb-pool, which allows for concurrent queries over a pool of connections while maintaining the same interface

TODO (in order of priority, from high to low):
- Add Gulp tasks to handle automatic building, tests and running the server in order to deprecate using npm
- Implement decorator to execute the SQL queries and reduce code duplication.
- Implement SQL query builder (e.g. squel.js) to erradicate the need for embedding SQL directly into the code (which is error-prone).
- Change enrollments route not to use route chaining in order to decide which SQL query is appropriate to respond the user's request.
- Implement decorator for API responses and also reduce code duplication.
- Split up tests into and add more test cases.

Signed-off-by: João Victor Risso <jvtr12@c3sl.ufpr.br>
---
 .babelrc                         |   1 +
 .eslintrc.json                   |  13 ++
 .gitignore                       |   1 -
 .gitlab-ci.yml                   |   2 +-
 build/config.json                |  16 ++
 build/libs/app.js                |  43 +++++
 build/libs/config.js             |   7 +
 build/libs/db/monet.js           |  24 +++
 build/libs/db/query_decorator.js |  22 +++
 build/libs/enrollment/all.js     |   1 +
 build/libs/enrollment/city.js    |   1 +
 build/libs/enrollment/common.js  |  15 ++
 build/libs/enrollment/country.js |   1 +
 build/libs/enrollment/region.js  |   1 +
 build/libs/enrollment/state.js   |   1 +
 build/libs/log.js                |  33 ++++
 build/libs/query_decorator.js    |  22 +++
 build/libs/routes/api.js         | 308 +++++++++++++++++++++++++++++++
 build/libs/routes/cities.js      |  85 +++++++++
 build/libs/routes/regions.js     |  54 ++++++
 build/libs/routes/states.js      |  70 +++++++
 build/logs/.gitignore            |   1 +
 build/server.js                  |  18 ++
 build/test/test.js               | 139 ++++++++++++++
 config.json                      |   2 +-
 gulpfile.babel.js                |  15 ++
 libs/app.js                      |  53 ------
 libs/config.js                   |   9 -
 libs/db/monet.js                 |  19 --
 libs/log.js                      |  35 ----
 libs/routes/cities.js            |  66 -------
 libs/routes/regions.js           |  45 -----
 libs/routes/states.js            |  55 ------
 package.json                     |  13 +-
 server.js                        |  13 --
 src/libs/app.js                  |  41 ++++
 src/libs/config.js               |   7 +
 src/libs/db/monet.js             |  22 +++
 src/libs/db/query_decorator.js   |  25 +++
 src/libs/enrollment/all.js       |   0
 src/libs/enrollment/city.js      |   0
 src/libs/enrollment/common.js    |  14 ++
 src/libs/enrollment/country.js   |   0
 src/libs/enrollment/region.js    |   0
 src/libs/enrollment/state.js     |   0
 src/libs/log.js                  |  34 ++++
 {libs => src/libs}/routes/api.js | 193 ++++++++++---------
 src/libs/routes/cities.js        |  95 ++++++++++
 src/libs/routes/regions.js       |  58 ++++++
 src/libs/routes/states.js        |  77 ++++++++
 src/server.js                    |  16 ++
 src/test/test.js                 | 155 ++++++++++++++++
 test/test.js                     | 158 ----------------
 53 files changed, 1544 insertions(+), 555 deletions(-)
 create mode 100644 .babelrc
 create mode 100644 .eslintrc.json
 create mode 100644 build/config.json
 create mode 100644 build/libs/app.js
 create mode 100644 build/libs/config.js
 create mode 100644 build/libs/db/monet.js
 create mode 100644 build/libs/db/query_decorator.js
 create mode 100644 build/libs/enrollment/all.js
 create mode 100644 build/libs/enrollment/city.js
 create mode 100644 build/libs/enrollment/common.js
 create mode 100644 build/libs/enrollment/country.js
 create mode 100644 build/libs/enrollment/region.js
 create mode 100644 build/libs/enrollment/state.js
 create mode 100644 build/libs/log.js
 create mode 100644 build/libs/query_decorator.js
 create mode 100644 build/libs/routes/api.js
 create mode 100644 build/libs/routes/cities.js
 create mode 100644 build/libs/routes/regions.js
 create mode 100644 build/libs/routes/states.js
 create mode 100644 build/logs/.gitignore
 create mode 100644 build/server.js
 create mode 100644 build/test/test.js
 create mode 100644 gulpfile.babel.js
 delete mode 100644 libs/app.js
 delete mode 100644 libs/config.js
 delete mode 100644 libs/db/monet.js
 delete mode 100644 libs/log.js
 delete mode 100644 libs/routes/cities.js
 delete mode 100644 libs/routes/regions.js
 delete mode 100644 libs/routes/states.js
 delete mode 100644 server.js
 create mode 100644 src/libs/app.js
 create mode 100644 src/libs/config.js
 create mode 100644 src/libs/db/monet.js
 create mode 100644 src/libs/db/query_decorator.js
 create mode 100644 src/libs/enrollment/all.js
 create mode 100644 src/libs/enrollment/city.js
 create mode 100644 src/libs/enrollment/common.js
 create mode 100644 src/libs/enrollment/country.js
 create mode 100644 src/libs/enrollment/region.js
 create mode 100644 src/libs/enrollment/state.js
 create mode 100644 src/libs/log.js
 rename {libs => src/libs}/routes/api.js (65%)
 create mode 100644 src/libs/routes/cities.js
 create mode 100644 src/libs/routes/regions.js
 create mode 100644 src/libs/routes/states.js
 create mode 100644 src/server.js
 create mode 100644 src/test/test.js
 delete mode 100644 test/test.js

diff --git a/.babelrc b/.babelrc
new file mode 100644
index 00000000..834a48a4
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1 @@
+{ 'presets': ['es2015'] }
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 00000000..52b136d7
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,13 @@
+{
+    "extends": "airbnb",
+    "plugins": [
+        "react",
+        "jsx-a11y",
+        "import"
+    ],
+    "rules": {
+        "indent": [ "error", 4 ],
+        "no-unused-vars": [ "error", { "args": "none" }],
+		"no-param-reassign": [ "off" ]
+    }
+}
diff --git a/.gitignore b/.gitignore
index 1be0326a..f4e4627c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,6 @@ lib-cov
 *.gz
 
 pids
-logs
 results
 
 npm-debug.log
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3171c9f7..a3c44c8f 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -7,6 +7,6 @@ before_script:
 run_tests:
   stage: test
   script:
-    - npm test
+    - gulp && cd build/ && mocha
   tags:
     - node
diff --git a/build/config.json b/build/config.json
new file mode 100644
index 00000000..c919cf1c
--- /dev/null
+++ b/build/config.json
@@ -0,0 +1,16 @@
+{
+    "port": 3000,
+    "monetdb": {
+        "host": "localhost",
+        "port": 50000,
+        "dbname": "simcaq_dev",
+        "user": "monetdb",
+        "password":"monetdb",
+        "nrConnections": 16
+    },
+    "default": {
+        "api": {
+            "version" : "v1"
+        }
+    }
+}
diff --git a/build/libs/app.js b/build/libs/app.js
new file mode 100644
index 00000000..a678b1c4
--- /dev/null
+++ b/build/libs/app.js
@@ -0,0 +1,43 @@
+'use strict';
+
+var express = require('express');
+var cookieParser = require('cookie-parser');
+var bodyParser = require('body-parser');
+var methodOverride = require('method-override');
+var cors = require('cors');
+
+var log = require('./log')(module);
+
+var api = require('./routes/api');
+var states = require('./routes/states');
+var regions = require('./routes/regions');
+var cities = require('./routes/cities');
+
+var app = express();
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(cors());
+app.use(methodOverride());
+
+app.use('/v1/', api);
+app.use('/v1/states', states);
+app.use('/v1/regions', regions);
+app.use('/v1/cities', cities);
+
+// catch 404 and forward to error handler
+app.use(function (req, res, next) {
+    res.status(404);
+    log.debug('%s %d %s', req.method, res.statusCode, req.url);
+    res.json({ error: 'Not found' }).end();
+});
+
+// error handlers
+app.use(function (err, req, res, next) {
+    res.status(err.status || 500);
+    log.error('%s %d %s', req.method, res.statusCode, err.message);
+    res.json({ error: err.message }).end();
+});
+
+module.exports = app;
\ No newline at end of file
diff --git a/build/libs/config.js b/build/libs/config.js
new file mode 100644
index 00000000..2f7bd18e
--- /dev/null
+++ b/build/libs/config.js
@@ -0,0 +1,7 @@
+'use strict';
+
+var nconf = require('nconf');
+
+nconf.argv().env().file({ file: process.cwd() + '/config.json' });
+
+module.exports = nconf;
\ No newline at end of file
diff --git a/build/libs/db/monet.js b/build/libs/db/monet.js
new file mode 100644
index 00000000..c8d29099
--- /dev/null
+++ b/build/libs/db/monet.js
@@ -0,0 +1,24 @@
+'use strict';
+
+var MonetDBPool = require('monetdb-pool');
+
+var libs = process.cwd() + '/libs';
+
+var config = require(libs + '/config');
+
+var poolOptions = {
+    nrConnections: config.get('monetdb:nrConnections')
+};
+
+var options = {
+    host: config.get('monetdb:host'),
+    port: config.get('monetdb:port'),
+    dbname: config.get('monetdb:dbname'),
+    user: config.get('monetdb:user'),
+    password: config.get('monetdb:password')
+};
+
+var conn = new MonetDBPool(poolOptions, options);
+conn.connect();
+
+module.exports = conn;
\ No newline at end of file
diff --git a/build/libs/db/query_decorator.js b/build/libs/db/query_decorator.js
new file mode 100644
index 00000000..ce0697d7
--- /dev/null
+++ b/build/libs/db/query_decorator.js
@@ -0,0 +1,22 @@
+"use strict";
+
+var libs = process.cwd() + "/libs";
+var log = require(libs + "/log")(module);
+var conn = require(libs + "/db/monet");
+
+/**
+ * Basic decorator to wrap SQL query strings
+ */
+exports.execQuery = function (sqlQuery, sqlQueryParams) {
+    log.debug("Executing SQL query '" + sqlQuery + "' with params '" + sqlQueryParams + "'");
+    conn.prepare(sqlQuery, true).then(function (dbQuery) {
+        dbQuery.exec(sqlQueryParams).then(function (dbResult) {
+            log.debug(dbResult.data);
+            log.debug("Query result: " + dbResult.data);
+            return dbResult;
+        }, function (error) {
+            log.error("SQL query execution error: " + error.message);
+            return error;
+        });
+    });
+};
\ No newline at end of file
diff --git a/build/libs/enrollment/all.js b/build/libs/enrollment/all.js
new file mode 100644
index 00000000..9a390c31
--- /dev/null
+++ b/build/libs/enrollment/all.js
@@ -0,0 +1 @@
+"use strict";
\ No newline at end of file
diff --git a/build/libs/enrollment/city.js b/build/libs/enrollment/city.js
new file mode 100644
index 00000000..9a390c31
--- /dev/null
+++ b/build/libs/enrollment/city.js
@@ -0,0 +1 @@
+"use strict";
\ No newline at end of file
diff --git a/build/libs/enrollment/common.js b/build/libs/enrollment/common.js
new file mode 100644
index 00000000..9c788951
--- /dev/null
+++ b/build/libs/enrollment/common.js
@@ -0,0 +1,15 @@
+'use strict';
+
+var libs = process.cwd() + '/libs';
+
+var log = require(libs + '/log')(module);
+
+var sqlDecorator = require(libs + '/query_decorator');
+
+var yearRange = function yearRange() {
+    var yearSql = 'SELECT MIN(t.ano_censo) AS start_year, MAX(t.ano_censo)' + 'AS end_year FROM turmas AS t';
+    log.debug('Generated SQL query for enrollments\' year range');
+    return sqlDecorator.execQuery(yearSql, []);
+};
+
+module.exports = yearRange;
\ No newline at end of file
diff --git a/build/libs/enrollment/country.js b/build/libs/enrollment/country.js
new file mode 100644
index 00000000..9a390c31
--- /dev/null
+++ b/build/libs/enrollment/country.js
@@ -0,0 +1 @@
+"use strict";
\ No newline at end of file
diff --git a/build/libs/enrollment/region.js b/build/libs/enrollment/region.js
new file mode 100644
index 00000000..9a390c31
--- /dev/null
+++ b/build/libs/enrollment/region.js
@@ -0,0 +1 @@
+"use strict";
\ No newline at end of file
diff --git a/build/libs/enrollment/state.js b/build/libs/enrollment/state.js
new file mode 100644
index 00000000..9a390c31
--- /dev/null
+++ b/build/libs/enrollment/state.js
@@ -0,0 +1 @@
+"use strict";
\ No newline at end of file
diff --git a/build/libs/log.js b/build/libs/log.js
new file mode 100644
index 00000000..e93d2ff2
--- /dev/null
+++ b/build/libs/log.js
@@ -0,0 +1,33 @@
+'use strict';
+
+var winston = require('winston');
+
+winston.emitErrs = true;
+
+function getFilePath(module) {
+    // using filename in log statements
+    return module.filename.split('/').slice(-2).join('/');
+}
+
+function logger(module) {
+    return new winston.Logger({
+        transports: [new winston.transports.File({
+            level: 'info',
+            filename: process.cwd() + '/logs/all.log',
+            handleException: true,
+            json: false,
+            maxSize: 5242880, // 5MB
+            maxFiles: 2,
+            colorize: false
+        }), new winston.transports.Console({
+            level: 'debug',
+            label: getFilePath(module),
+            handleException: true,
+            json: true,
+            colorize: true
+        })],
+        exitOnError: false
+    });
+}
+
+module.exports = logger;
\ No newline at end of file
diff --git a/build/libs/query_decorator.js b/build/libs/query_decorator.js
new file mode 100644
index 00000000..ce0697d7
--- /dev/null
+++ b/build/libs/query_decorator.js
@@ -0,0 +1,22 @@
+"use strict";
+
+var libs = process.cwd() + "/libs";
+var log = require(libs + "/log")(module);
+var conn = require(libs + "/db/monet");
+
+/**
+ * Basic decorator to wrap SQL query strings
+ */
+exports.execQuery = function (sqlQuery, sqlQueryParams) {
+    log.debug("Executing SQL query '" + sqlQuery + "' with params '" + sqlQueryParams + "'");
+    conn.prepare(sqlQuery, true).then(function (dbQuery) {
+        dbQuery.exec(sqlQueryParams).then(function (dbResult) {
+            log.debug(dbResult.data);
+            log.debug("Query result: " + dbResult.data);
+            return dbResult;
+        }, function (error) {
+            log.error("SQL query execution error: " + error.message);
+            return error;
+        });
+    });
+};
\ No newline at end of file
diff --git a/build/libs/routes/api.js b/build/libs/routes/api.js
new file mode 100644
index 00000000..812eae43
--- /dev/null
+++ b/build/libs/routes/api.js
@@ -0,0 +1,308 @@
+'use strict';
+
+var express = require('express');
+
+var xml = require('js2xmlparser');
+
+var enrollmentApp = express();
+
+var libs = process.cwd() + '/libs';
+
+var log = require(libs + '/log')(module);
+
+var conn = require(libs + '/db/monet');
+
+function response(req, res) {
+    if (req.query.format === 'csv') {
+        res.csv(req.result.data);
+    } else if (req.query.format === 'xml') {
+        res.send(xml('result', JSON.stringify({ state: req.result.data })));
+    } else {
+        res.json({ result: req.result.data });
+    }
+}
+
+enrollmentApp.get('/', function (req, res) {
+    res.json({ msg: 'SimCAQ API is running' });
+});
+
+/**
+ * Complete range of the enrollments dataset
+ *
+ * Returns a tuple of start and ending years of the complete enrollments dataset.
+ */
+enrollmentApp.get('/year_range', function (req, res) {
+    var yearSql = 'SELECT MIN(t.ano_censo) AS start_year, MAX(t.ano_censo)' + 'AS end_year FROM turmas AS t';
+
+    conn.query(yearSql, true).then(function (dbResult) {
+        log.debug(dbResult);
+        req.result = dbResult;
+        response(req, res);
+    }, function (dbError) {
+        log.error('SQL query execution error: ' + dbError.message);
+        // FIXME: change response to HTTP 501 status
+        res.json({ error: 'An internal error has occurred' }).end();
+    });
+});
+
+enrollmentApp.get('/education_level', function (req, res) {
+    var edLevelSql = 'SELECT pk_etapa_ensino_id AS id, desc_etapa AS ' + 'education_level FROM etapa_ensino';
+    conn.query(edLevelSql, true).then(function (dbResult) {
+        log.debug(dbResult);
+        req.result = dbResult;
+        response(req, res);
+    }, function (dbError) {
+        log.error('SQL query error: ' + dbError.message);
+        // FIXME: change response to HTTP 501 status
+        res.json({ error: 'An internal error has occurred' }).end();
+    });
+});
+
+enrollmentApp.get('/data', function (req, res) {
+    log.debug(req.query);
+    log.debug(req.query.met);
+    log.debug(req.query.dim);
+    var schoolClassSql = 'SELECT * FROM turmas';
+    conn.query(schoolClassSql, true).then(function (dbResult) {
+        log.debug(dbResult);
+        req.result = dbResult;
+        response(req, res);
+    }, function (dbError) {
+        log.error('SQL query error: ' + dbError.message);
+        // FIXME: change response to HTTP 501 status
+        res.json({ error: 'An internal error has occurred' }).end();
+    });
+});
+
+enrollmentApp.use('/enrollments', function (req, res, next) {
+    var params = req.query;
+    req.paramCnt = 0;
+
+    if (typeof params.id !== 'undefined') {
+        req.id = parseInt(params.id, 10);
+        req.paramCnt += 1;
+    }
+
+    if (typeof params.location_id !== 'undefined') {
+        req.location_id = parseInt(params.location_id, 10);
+        req.paramCnt += 1;
+    }
+
+    if (typeof params.adm_dependency_id !== 'undefined') {
+        req.adm_dependency_id = parseInt(params.adm_dependency_id, 10);
+        req.paramCnt += 1;
+    }
+
+    if (typeof params.census_year !== 'undefined') {
+        req.census_year = parseInt(params.census_year, 10);
+        req.paramCnt += 1;
+    }
+
+    if (typeof params.education_level_id !== 'undefined') {
+        req.education_level_id = parseInt(params.education_level_id, 10);
+        req.paramCnt += 1;
+    }
+
+    next();
+});
+
+enrollmentApp.use('/enrollments', function (req, res, next) {
+    var params = req.query;
+    if (typeof params.aggregate !== 'undefined' && params.aggregate === 'region') {
+        log.debug('Using enrollments query for regions');
+        req.sqlQuery = 'SELECT r.nome AS name, COALESCE(SUM(t.num_matriculas), 0) AS total ' + 'FROM regioes AS r ' + 'INNER JOIN estados AS e ON r.pk_regiao_id = e.fk_regiao_id ' + 'INNER JOIN municipios AS m ON e.pk_estado_id = m.fk_estado_id ' + 'LEFT OUTER JOIN turmas AS t ON ( ' + 'm.pk_municipio_id = t.fk_municipio_id ';
+        req.sqlQueryParams = [];
+
+        if (typeof req.census_year !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.ano_censo = ?';
+            req.sqlQueryParams.push(req.census_year);
+        }
+
+        if (typeof req.adm_dependency_id !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.fk_dependencia_adm_id = ?';
+            req.sqlQueryParams.push(req.adm_dependency_id);
+        }
+
+        if (typeof req.location_id !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.id_localizacao = ?';
+            req.sqlQueryParams.push(req.location_id);
+        }
+
+        if (typeof req.education_level_id !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.fk_etapa_ensino_id = ?';
+            req.sqlQueryParams.push(req.education_level_id);
+        }
+
+        req.sqlQuery += ')';
+        if (typeof req.id !== 'undefined') {
+            req.sqlQuery += ' WHERE ';
+            req.sqlQuery += 'r.pk_regiao_id = ?';
+            req.sqlQueryParams.push(req.id);
+        }
+        req.sqlQuery += ' GROUP BY r.nome';
+    }
+    next();
+});
+
+enrollmentApp.use('/enrollments', function (req, res, next) {
+    var params = req.query;
+    if (typeof params.aggregate !== 'undefined' && params.aggregate === 'state') {
+        log.debug('Using enrollments query for states');
+        req.sqlQuery = 'SELECT e.nome AS name, COALESCE(SUM(t.num_matriculas), 0) as total ' + 'FROM estados AS e ' + 'INNER JOIN municipios AS m ON m.fk_estado_id = e.pk_estado_id ' + 'LEFT OUTER JOIN turmas AS t ON (' + 'm.pk_municipio_id = t.fk_municipio_id ';
+        req.sqlQueryParams = [];
+
+        if (typeof req.census_year !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.ano_censo = ?';
+            req.sqlQueryParams.push(req.census_year);
+        }
+
+        if (typeof req.adm_dependency_id !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.fk_dependencia_adm_id = ?';
+            req.sqlQueryParams.push(req.adm_dependency_id);
+        }
+
+        if (typeof req.location_id !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.id_localizacao = ?';
+            req.sqlQueryParams.push(req.location_id);
+        }
+
+        if (typeof req.education_level_id !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.fk_etapa_ensino_id = ?';
+            req.sqlQueryParams.push(req.education_level_id);
+        }
+
+        req.sqlQuery += ')';
+
+        if (typeof req.id !== 'undefined') {
+            req.sqlQuery += ' WHERE ';
+            req.sqlQuery += 'e.pk_estado_id = ?';
+            req.sqlQueryParams.push(req.id);
+        }
+
+        req.sqlQuery += ' GROUP BY e.nome';
+    }
+    next();
+});
+
+enrollmentApp.use('/enrollments', function (req, res, next) {
+    var params = req.query;
+    if (typeof params.aggregate !== 'undefined' && params.aggregate === 'city') {
+        log.debug('Using enrollments query for cities');
+        req.sqlQuery = 'SELECT m.nome AS name, COALESCE(SUM(t.num_matriculas), 0) as total ' + 'FROM municipios AS m ' + 'LEFT OUTER JOIN turmas AS t ON ( ' + 'm.pk_municipio_id = t.fk_municipio_id';
+        req.sqlQueryParams = [];
+
+        if (typeof req.census_year !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.ano_censo = ?';
+            req.sqlQueryParams.push(req.census_year);
+        }
+
+        if (typeof req.adm_dependency_id !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.fk_dependencia_adm_id = ?';
+            req.sqlQueryParams.push(req.adm_dependency_id);
+        }
+
+        if (typeof req.location_id !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.id_localizacao = ?';
+            req.sqlQueryParams.push(req.location_id);
+        }
+
+        if (typeof req.education_level_id !== 'undefined') {
+            req.sqlQuery += ' AND ';
+            req.sqlQuery += 't.fk_etapa_ensino_id = ?';
+            req.sqlQueryParams.push(req.education_level_id);
+        }
+
+        req.sqlQuery += ')';
+
+        if (typeof req.id !== 'undefined') {
+            req.sqlQuery += ' WHERE ';
+            req.sqlQuery += 'm.pk_municipio_id = ?';
+            req.sqlQueryParams.push(req.id);
+        }
+
+        req.sqlQuery += 'GROUP BY m.nome';
+    }
+    next();
+});
+
+enrollmentApp.use('/enrollments', function (req, res, next) {
+    var params = req.query;
+    if (typeof params.aggregate === 'undefined') {
+        log.debug('Using enrollments query for the whole country');
+        req.sqlQuery = 'SELECT \'Brasil\' AS name, COALESCE(SUM(t.num_matriculas),0) AS total ' + 'FROM turmas AS t';
+        req.sqlQueryParams = [];
+
+        if (req.paramCnt > 0) {
+            req.sqlQuery += ' WHERE ';
+        }
+
+        if (typeof req.census_year !== 'undefined') {
+            req.sqlQuery += 't.ano_censo = ?';
+            req.sqlQueryParams.push(req.census_year);
+        }
+
+        if (typeof req.adm_dependency_id !== 'undefined') {
+            if (req.sqlQueryParams.length > 0) {
+                req.sqlQuery += ' AND ';
+            }
+            req.sqlQuery += 't.fk_dependencia_adm_id = ?';
+            req.sqlQueryParams.push(req.adm_dependency_id);
+        }
+
+        if (typeof req.location_id !== 'undefined') {
+            if (req.sqlQueryParams.length > 0) {
+                req.sqlQuery += ' AND ';
+            }
+            req.sqlQuery += 't.id_localizacao = ?';
+            req.sqlQueryParams.push(req.location_id);
+        }
+
+        if (typeof req.education_level_id !== 'undefined') {
+            if (req.sqlQueryParams.length > 0) {
+                req.sqlQuery += ' AND ';
+            }
+            req.sqlQuery += 't.fk_etapa_ensino_id = ?';
+            req.sqlQueryParams.push(req.education_level_id);
+        }
+    }
+    next();
+});
+
+enrollmentApp.get('/enrollments', function (req, res, next) {
+    log.debug('Request parameters: ' + req);
+    if (typeof req.sqlQuery === 'undefined') {
+        // Should only happen if there is a bug in the chaining of the
+        // '/enrollments' route, since when no +aggregate+ parameter is given,
+        // it defaults to use the query for the whole country.
+        log.error('BUG -- No SQL query was found to be executed!');
+        res.send('Request could not be satisfied due to an internal error');
+    } else {
+        log.debug('SQL query: ${ req.sqlQuery }?');
+        log.debug(req.sqlQuery);
+
+        conn.prepare(req.sqlQuery, true).then(function (dbQuery) {
+            dbQuery.exec(req.sqlQueryParams).then(function (dbResult) {
+                log.debug(dbResult);
+                req.result = dbResult;
+                response(req, res);
+            }, function (dbError) {
+                log.error('SQL query execution error: ' + dbError.message);
+                // FIXME: change response to HTTP 501 status
+                res.json({ error: 'An internal error has occurred' }).end();
+            });
+        });
+    }
+});
+
+module.exports = enrollmentApp;
\ No newline at end of file
diff --git a/build/libs/routes/cities.js b/build/libs/routes/cities.js
new file mode 100644
index 00000000..f549624e
--- /dev/null
+++ b/build/libs/routes/cities.js
@@ -0,0 +1,85 @@
+'use strict';
+
+var express = require('express');
+
+var xml = require('js2xmlparser');
+
+var cityApp = express();
+
+var libs = process.cwd() + '/libs';
+
+var log = require(libs + '/log')(module);
+
+var conn = require(libs + '/db/monet');
+
+function response(req, res) {
+    if (req.query.format === 'csv') {
+        res.csv(req.result.data);
+    } else if (req.query.format === 'xml') {
+        res.send(xml('result', JSON.stringify({ city: req.result.data })));
+    } else {
+        res.json({ result: req.result.data });
+    }
+}
+
+cityApp.get('/', function (req, res) {
+    conn.query('SELECT * FROM municipios', true).then(function (dbResult) {
+        log.debug(dbResult);
+        req.result = dbResult;
+        response(req, res);
+    }, function (dbError) {
+        log.error('SQL query execution error: ' + dbError.message);
+        // FIXME: change response to HTTP 501 status
+        res.json({ error: 'An internal error has occurred' }).end();
+    });
+});
+
+cityApp.get('/:id', function (req, res) {
+    var citySql = 'SELECT * FROM municipios WHERE pk_municipio_id = ?';
+    var cityId = parseInt(req.params.id, 10);
+    conn.prepare(citySql, true).then(function (dbQuery) {
+        dbQuery.exec([cityId]).then(function (dbResult) {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        }, function (dbError) {
+            log.error('SQL query execution error: ' + dbError.message);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        });
+    });
+});
+
+cityApp.get('/ibge/:id', function (req, res) {
+    var citySql = 'SELECT * FROM municipios WHERE codigo_ibge = ?';
+    var cityId = parseInt(req.params.id, 10);
+    conn.prepare(citySql, true).then(function (dbQuery) {
+        dbQuery.exec([cityId]).then(function (dbResult) {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        }, function (dbError) {
+            log.error('SQL query execution error: ' + dbError.message);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        });
+    });
+});
+
+cityApp.get('/state/:id', function (req, res) {
+    var citySql = 'SELECT * FROM municipios WHERE fk_estado_id = ?';
+    var stateId = parseInt(req.params.id, 10);
+    conn.prepare(citySql, true).then(function (dbQuery) {
+        dbQuery.exec([stateId]).then(function (dbResult) {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        }, function (dbError) {
+            log.error('SQL query execution error: ' + dbError.message);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        });
+    });
+});
+
+module.exports = cityApp;
\ No newline at end of file
diff --git a/build/libs/routes/regions.js b/build/libs/routes/regions.js
new file mode 100644
index 00000000..75cf2bee
--- /dev/null
+++ b/build/libs/routes/regions.js
@@ -0,0 +1,54 @@
+'use strict';
+
+var express = require('express');
+
+var xml = require('js2xmlparser');
+
+var regionApp = express();
+
+var libs = process.cwd() + '/libs';
+
+var log = require(libs + '/log')(module);
+
+var conn = require(libs + '/db/monet');
+
+function response(req, res) {
+    if (req.query.format === 'csv') {
+        res.csv(req.result.data);
+    } else if (req.query.format === 'xml') {
+        res.send(xml('result', JSON.stringify({ state: req.result.data })));
+    } else {
+        res.json({ result: req.result.data });
+    }
+}
+
+regionApp.get('/', function (req, res) {
+    var regionSql = 'SELECT * FROM regioes';
+    conn.query(regionSql, true).then(function (dbResult) {
+        log.debug(dbResult);
+        req.result = dbResult;
+        response(req, res);
+    }, function (dbError) {
+        log.error('SQL query execution error: ' + dbError.message);
+        // FIXME: change response to HTTP 501 status
+        res.json({ error: 'An internal error has occurred' }).end();
+    });
+});
+
+regionApp.get('/:id', function (req, res) {
+    var regionSql = 'SELECT * FROM regioes WHERE pk_regiao_id = ?';
+    var regionId = parseInt(req.params.id, 10);
+    conn.prepare(regionSql, true).then(function (dbQuery) {
+        dbQuery.exec([regionId]).then(function (dbResult) {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        }, function (dbError) {
+            log.error('SQL query execution error: ' + dbError.message);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        });
+    });
+});
+
+module.exports = regionApp;
\ No newline at end of file
diff --git a/build/libs/routes/states.js b/build/libs/routes/states.js
new file mode 100644
index 00000000..8579f392
--- /dev/null
+++ b/build/libs/routes/states.js
@@ -0,0 +1,70 @@
+'use strict';
+
+var express = require('express');
+
+var xml = require('js2xmlparser');
+
+var stateApp = express();
+
+var libs = process.cwd() + '/libs';
+
+var log = require(libs + '/log')(module);
+
+var conn = require(libs + '/db/monet');
+
+function response(req, res) {
+    if (req.query.format === 'csv') {
+        res.csv(req.result.data);
+    } else if (req.query.format === 'xml') {
+        res.send(xml('result', JSON.stringify({ state: req.result.data })));
+    } else {
+        res.json({ result: req.result.data });
+    }
+}
+
+stateApp.get('/', function (req, res, next) {
+    var stateSql = 'SELECT * FROM estados';
+    conn.query(stateSql, true).then(function (dbResult) {
+        log.debug(dbResult);
+        req.result = dbResult;
+        response(req, res);
+    }, function (dbError) {
+        log.error('SQL query execution error: ' + dbError.message);
+        // FIXME: change response to HTTP 501 status
+        res.json({ error: 'An internal error has occurred' }).end();
+    });
+});
+
+stateApp.get('/:id', function (req, res, next) {
+    var stateSql = 'SELECT * FROM estados WHERE pk_estado_id = ?';
+    var stateId = parseInt(req.params.id, 10);
+    conn.prepare(stateSql, true).then(function (dbQuery) {
+        dbQuery.exec([stateId]).then(function (dbResult) {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        }, function (dbError) {
+            log.error('SQL query execution error: ' + dbError.message);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        });
+    });
+});
+
+stateApp.get('/region/:id', function (req, res, next) {
+    var stateSql = 'SELECT * FROM estados WHERE fk_regiao_id = ?';
+    var regionId = parseInt(req.params.id, 10);
+    conn.prepare(stateSql, true).then(function (dbQuery) {
+        dbQuery.exec([regionId]).then(function (dbResult) {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        }, function (dbError) {
+            log.error('SQL query execution error: ' + dbError.message);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        });
+    });
+});
+
+module.exports = stateApp;
\ No newline at end of file
diff --git a/build/logs/.gitignore b/build/logs/.gitignore
new file mode 100644
index 00000000..397b4a76
--- /dev/null
+++ b/build/logs/.gitignore
@@ -0,0 +1 @@
+*.log
diff --git a/build/server.js b/build/server.js
new file mode 100644
index 00000000..5f2171cc
--- /dev/null
+++ b/build/server.js
@@ -0,0 +1,18 @@
+'use strict';
+
+var debug = require('debug')('node-express-base');
+
+var libs = process.cwd() + '/libs';
+
+var config = require(libs + '/config');
+
+var log = require(libs + '/log')(module);
+
+var app = require(libs + '/app');
+
+app.set('port', config.get('port') || 3000);
+
+var server = app.listen(app.get('port'), function () {
+    debug('Express server listening on port ' + server.address().port);
+    log.info('Express server listening on port ' + config.get('port'));
+});
\ No newline at end of file
diff --git a/build/test/test.js b/build/test/test.js
new file mode 100644
index 00000000..55caa55c
--- /dev/null
+++ b/build/test/test.js
@@ -0,0 +1,139 @@
+'use strict';
+
+var chai = require('chai');
+var chaiHttp = require('chai-http');
+var assert = chai.assert;
+var expect = chai.expect;
+var should = chai.should(); // actually call the function
+var server = require('../libs/app');
+
+chai.use(chaiHttp);
+
+describe('request enrollments', function () {
+    it('should list enrollments', function (done) {
+        chai.request(server).get('/v1/enrollments').end(function (err, res) {
+            res.should.have.status(200);
+            res.should.be.json;
+            res.body.should.have.property('result');
+            res.body.result.should.be.a('array');
+            res.body.result[0].should.have.property('name');
+            res.body.result[0].should.have.property('total');
+            done();
+        });
+    });
+});
+
+describe('request regions', function () {
+    it('should list all regions', function (done) {
+        chai.request(server).get('/v1/regions').end(function (err, res) {
+            res.should.have.status(200);
+            res.should.be.json;
+            res.body.should.have.property('result');
+            res.body.result.should.be.a('array');
+            res.body.result[0].should.have.property('pk_regiao_id');
+            res.body.result[0].should.have.property('nome');
+            done();
+        });
+    });
+
+    it('should list region by id', function (done) {
+        chai.request(server).get('/v1/regions/1').end(function (err, res) {
+            res.should.have.status(200);
+            res.should.be.json;
+            res.body.should.have.property('result');
+            res.body.result.should.be.a('array');
+            res.body.result.should.have.length(1);
+            res.body.result[0].should.have.property('pk_regiao_id');
+            res.body.result[0].should.have.property('nome');
+            done();
+        });
+    });
+});
+
+describe('request states', function () {
+
+    it('should list all states', function (done) {
+        chai.request(server).get('/v1/states').end(function (err, res) {
+            res.should.have.status(200);
+            res.should.be.json;
+            res.body.should.have.property('result');
+            res.body.result.should.be.a('array');
+            res.body.result[0].should.have.property('pk_estado_id');
+            res.body.result[0].should.have.property('fk_regiao_id');
+            res.body.result[0].should.have.property('nome');
+            done();
+        });
+    });
+
+    it('should list a state by id', function (done) {
+        chai.request(server).get('/v1/states/11').end(function (err, res) {
+            res.should.have.status(200);
+            res.should.be.json;
+            res.body.should.have.property('result');
+            res.body.result.should.be.a('array');
+            res.body.result.should.have.length(1);
+            res.body.result[0].should.have.property('pk_estado_id');
+            res.body.result[0].should.have.property('fk_regiao_id');
+            res.body.result[0].should.have.property('nome');
+            done();
+        });
+    });
+
+    it('should list states by region id', function (done) {
+        chai.request(server).get('/v1/states/region/1').end(function (err, res) {
+            res.should.have.status(200);
+            res.should.be.json;
+            res.body.should.have.property('result');
+            res.body.result.should.be.a('array');
+            res.body.result[0].should.have.property('pk_estado_id');
+            res.body.result[0].should.have.property('fk_regiao_id');
+            res.body.result[0].should.have.property('nome');
+            done();
+        });
+    });
+});
+
+describe('request cities', function () {
+
+    it('should list all cities', function (done) {
+        chai.request(server).get('/v1/cities').end(function (err, res) {
+            res.should.have.status(200);
+            res.should.be.json;
+            res.body.should.have.property('result');
+            res.body.result.should.be.a('array');
+            res.body.result[0].should.have.property('pk_municipio_id');
+            res.body.result[0].should.have.property('fk_estado_id');
+            res.body.result[0].should.have.property('nome');
+            res.body.result[0].should.have.property('codigo_ibge');
+            done();
+        });
+    });
+
+    it('should list a city by id', function (done) {
+        chai.request(server).get('/v1/cities/1').end(function (err, res) {
+            res.should.have.status(200);
+            res.should.be.json;
+            res.body.should.have.property('result');
+            res.body.result.should.be.a('array');
+            res.body.result[0].should.have.property('pk_municipio_id');
+            res.body.result[0].should.have.property('fk_estado_id');
+            res.body.result[0].should.have.property('nome');
+            res.body.result[0].should.have.property('codigo_ibge');
+            done();
+        });
+    });
+
+    it('should list a city by codigo_ibge', function (done) {
+        chai.request(server).get('/v1/cities/ibge/1200013').end(function (err, res) {
+            res.should.have.status(200);
+            res.should.be.json;
+            res.body.should.have.property('result');
+            res.body.result.should.be.a('array');
+            res.body.result[0].should.have.property('pk_municipio_id');
+            res.body.result[0].should.have.property('fk_estado_id');
+            res.body.result[0].should.have.property('nome');
+            res.body.result[0].should.have.property('codigo_ibge');
+            done();
+        });
+    });
+});
\ No newline at end of file
diff --git a/config.json b/config.json
index 8f60036a..1da8951e 100644
--- a/config.json
+++ b/config.json
@@ -1,7 +1,7 @@
 {
     "port": 3000,
     "monetdb": {
-        "host": "simcaqdb1",
+        "host": "localhost",
         "port": 50000,
         "dbname": "simcaq_dev",
         "user": "monetdb",
diff --git a/gulpfile.babel.js b/gulpfile.babel.js
new file mode 100644
index 00000000..e07481f9
--- /dev/null
+++ b/gulpfile.babel.js
@@ -0,0 +1,15 @@
+const gulp = require('gulp');
+const babel = require('gulp-babel');
+const eslint = require('gulp-eslint');
+
+gulp.task('default', () => {
+    // run ESLint
+    gulp.src('src/**/*.js')
+        .pipe(eslint())
+        .pipe(eslint.format());
+
+    // compile source to ES5
+    gulp.src('src/**/*.js')
+        .pipe(babel())
+        .pipe(gulp.dest('build'));
+});
diff --git a/libs/app.js b/libs/app.js
deleted file mode 100644
index 544f649b..00000000
--- a/libs/app.js
+++ /dev/null
@@ -1,53 +0,0 @@
-var express = require('express')
-var path = require('path')
-var cookieParser = require('cookie-parser')
-var bodyParser = require('body-parser')
-var csv = require('csv-express')
-var xml = require('js2xmlparser')
-var methodOverride = require('method-override')
-var cors = require('cors')
-
-var libs = process.cwd() + '/libs/'
-
-var config = require('./config')
-var log = require('./log')(module)
-
-var api = require('./routes/api')
-var states = require('./routes/states')
-var regions = require('./routes/regions')
-var cities = require('./routes/cities')
-
-var app = express()
-
-app.use(bodyParser.json())
-app.use(bodyParser.urlencoded({ extended: false }))
-app.use(cookieParser())
-app.use(cors())
-app.use(methodOverride())
-
-app.use('/v1/', api)
-app.use('/v1/states', states)
-app.use('/v1/regions', regions)
-app.use('/v1/cities', cities)
-
-// catch 404 and forward to error handler
-app.use(function(req, res, next){
-    res.status(404)
-    log.debug('%s %d %s', req.method, res.statusCode, req.url)
-    res.json({
-    	error: 'Not found'
-    })
-    return
-})
-
-// error handlers
-app.use(function(err, req, res, next){
-    res.status(err.status || 500)
-    log.error('%s %d %s', req.method, res.statusCode, err.message)
-    res.json({
-    	error: err.message
-    })
-    return
-})
-
-module.exports = app
diff --git a/libs/config.js b/libs/config.js
deleted file mode 100644
index 78f7831a..00000000
--- a/libs/config.js
+++ /dev/null
@@ -1,9 +0,0 @@
-var nconf = require('nconf')
-
-nconf.argv()
-	.env()
-	.file({
-		file: process.cwd() + '/config.json'
-	})
-
-module.exports = nconf
diff --git a/libs/db/monet.js b/libs/db/monet.js
deleted file mode 100644
index 0b554285..00000000
--- a/libs/db/monet.js
+++ /dev/null
@@ -1,19 +0,0 @@
-var mdb = require('monetdb')()
-
-var libs = process.cwd() + '/libs/'
-
-var log = require(libs + 'log')(module)
-var config = require(libs + 'config')
-
-var options = {
-    host: config.get('monetdb:host'),
-    port: config.get('monetdb:port'),
-    dbname: config.get('monetdb:dbname'),
-    user: config.get('monetdb:user'),
-    password: config.get('monetdb:password')
-}
-
-var conn = new mdb(options)
-conn.connect()
-
-module.exports = conn
diff --git a/libs/log.js b/libs/log.js
deleted file mode 100644
index 419b3e4b..00000000
--- a/libs/log.js
+++ /dev/null
@@ -1,35 +0,0 @@
-var winston = require('winston')
-
-winston.emitErrs = true
-
-function logger(module) {
-
-    return new winston.Logger({
-        transports : [
-            new winston.transports.File({
-                level: 'info',
-                filename: process.cwd() + '/logs/all.log',
-                handleException: true,
-                json: false,
-                maxSize: 5242880, //5mb
-                maxFiles: 2,
-                colorize: false
-            }),
-            new winston.transports.Console({
-                level: 'debug',
-                label: getFilePath(module),
-                handleException: true,
-                json: true,
-                colorize: true
-            })
-        ],
-        exitOnError: false
-    })
-}
-
-function getFilePath (module ) {
-    //using filename in log statements
-    return module.filename.split('/').slice(-2).join('/')
-}
-
-module.exports = logger
diff --git a/libs/routes/cities.js b/libs/routes/cities.js
deleted file mode 100644
index 6a222049..00000000
--- a/libs/routes/cities.js
+++ /dev/null
@@ -1,66 +0,0 @@
-var express = require('express')
-var xml = require('js2xmlparser')
-var router = express.Router()
-
-var libs = process.cwd() + '/libs/'
-
-var log = require(libs + 'log')(module)
-var config = require(libs + 'config')
-
-var conn = require(libs + 'db/monet')
-
-function response(req, res) {
-  if (req.query.format === 'csv') {
-    res.csv(req.result.data)
-  } else if (req.query.format === 'xml') {
-    res.send(xml("result", JSON.stringify({city: req.result.data})))
-  }
-  else {
-    res.json({
-        result: req.result.data
-    })
-  }
-}
-
-router.get('/', function(req, res) {
-  conn.query(
-    'SELECT * FROM municipios', true
-  ).then(function(result) {
-    log.debug(result)
-    req.result = result
-    response(req, res)
-  })
-
-})
-
-router.get('/:id', function(req, res) {
-  conn.query(
-    'SELECT * FROM municipios WHERE pk_municipio_id='+req.params.id, true
-  ).then(function(result) {
-    log.debug(result)
-    req.result = result
-    response(req, res)
-  })
-})
-
-router.get('/ibge/:id', function(req, res) {
-  conn.query(
-    'SELECT * FROM municipios WHERE codigo_ibge='+req.params.id, true
-  ).then(function(result) {
-    log.debug(result)
-    req.result = result
-    response(req, res)
-  })
-})
-
-router.get('/state/:id', function(req, res) {
-  conn.query(
-    'SELECT * FROM municipios WHERE fk_estado_id='+req.params.id, true
-  ).then(function(result) {
-    log.debug(result)
-    req.result = result
-    response(req, res)
-  })
-})
-
-module.exports = router
diff --git a/libs/routes/regions.js b/libs/routes/regions.js
deleted file mode 100644
index 6c85fccb..00000000
--- a/libs/routes/regions.js
+++ /dev/null
@@ -1,45 +0,0 @@
-var express = require('express')
-var xml = require('js2xmlparser')
-var router = express.Router()
-
-var libs = process.cwd() + '/libs/'
-
-var log = require(libs + 'log')(module)
-var config = require(libs + 'config')
-
-var conn = require(libs + 'db/monet')
-
-function response(req, res) {
-  if (req.query.format === 'csv') {
-    res.csv(req.result.data)
-  } else if (req.query.format === 'xml') {
-    res.send(xml("result", JSON.stringify({state: req.result.data})))
-  }
-  else {
-    res.json({
-        result: req.result.data
-    })
-  }
-}
-
-router.get('/', function(req, res) {
-  conn.query(
-    'SELECT * FROM regioes', true
-  ).then(function(result) {
-    log.debug(result)
-    req.result = result
-    response(req, res)
-  })
-})
-
-router.get('/:id', function(req, res) {
-  conn.query(
-    'SELECT * FROM regioes WHERE pk_regiao_id='+req.params.id, true
-  ).then(function(result) {
-    log.debug(result)
-    req.result = result
-    response(req, res)
-  })
-})
-
-module.exports = router
diff --git a/libs/routes/states.js b/libs/routes/states.js
deleted file mode 100644
index 2192c537..00000000
--- a/libs/routes/states.js
+++ /dev/null
@@ -1,55 +0,0 @@
-var express = require('express')
-var xml = require('js2xmlparser')
-var router = express.Router()
-
-var libs = process.cwd() + '/libs/'
-
-var log = require(libs + 'log')(module)
-var config = require(libs + 'config')
-
-var conn = require(libs + 'db/monet')
-
-function response(req, res) {
-  if (req.query.format === 'csv') {
-    res.csv(req.result.data)
-  } else if (req.query.format === 'xml') {
-    res.send(xml("result", JSON.stringify({state: req.result.data})))
-  }
-  else {
-    res.json({
-        result: req.result.data
-    })
-  }
-}
-
-router.get('/', function(req, res, next) {
-  conn.query(
-    'SELECT * FROM estados', true
-  ).then(function(result) {
-    log.debug(result)
-    req.result = result
-    response(req, res)
-  })
-})
-
-router.get('/:id', function(req, res, next) {
-  conn.query(
-    'SELECT * FROM estados WHERE pk_estado_id='+req.params.id, true
-  ).then(function(result) {
-    log.debug(result)
-    req.result = result
-    response(req, res)
-  })
-})
-
-router.get('/region/:id', function(req, res, next) {
-  conn.query(
-    'SELECT * FROM estados WHERE fk_regiao_id='+req.params.id, true
-  ).then(function(result) {
-    log.debug(result)
-    req.result = result
-    response(req, res)
-  })
-})
-
-module.exports = router
diff --git a/package.json b/package.json
index fd0765bb..cee6b8e0 100644
--- a/package.json
+++ b/package.json
@@ -19,14 +19,25 @@
     "forever": "^0.15.2",
     "js2xmlparser": "^1.0.0",
     "method-override": "^2.3.3",
-    "monetdb": "^1.1.2",
+    "monetdb-pool": "0.0.8",
     "nconf": "^0.6.x",
     "winston": "^2.2.0"
   },
   "license": "MIT",
   "devDependencies": {
+    "babel-preset-es2015": "^6.13.2",
+    "babelify": "^7.3.0",
+    "browserify": "^13.1.0",
     "chai": "^3.5.0",
     "chai-http": "^3.0.0",
+    "eslint": "^3.3.1",
+    "eslint-config-airbnb": "^10.0.1",
+    "eslint-plugin-import": "^1.13.0",
+    "eslint-plugin-jsx-a11y": "^2.1.0",
+    "eslint-plugin-react": "^6.1.1",
+    "gulp": "^3.9.1",
+    "gulp-babel": "^6.1.2",
+    "gulp-eslint": "^3.0.1",
     "mocha": "^2.5.3"
   }
 }
diff --git a/server.js b/server.js
deleted file mode 100644
index dc0c1ce3..00000000
--- a/server.js
+++ /dev/null
@@ -1,13 +0,0 @@
-var debug = require('debug')('node-express-base')
-
-var libs = process.cwd() + '/libs/'
-var config = require(libs + 'config')
-var log = require(libs + 'log')(module)
-var app = require(libs + 'app')
-
-app.set('port', config.get('port') || 3000)
-
-var server = app.listen(app.get('port'), function() {
-  debug('Express server listening on port ' + server.address().port)
-  log.info('Express server listening on port ' + config.get('port'))
-})
diff --git a/src/libs/app.js b/src/libs/app.js
new file mode 100644
index 00000000..c2396420
--- /dev/null
+++ b/src/libs/app.js
@@ -0,0 +1,41 @@
+const express = require('express');
+const cookieParser = require('cookie-parser');
+const bodyParser = require('body-parser');
+const methodOverride = require('method-override');
+const cors = require('cors');
+
+const log = require('./log')(module);
+
+const api = require('./routes/api');
+const states = require('./routes/states');
+const regions = require('./routes/regions');
+const cities = require('./routes/cities');
+
+const app = express();
+
+app.use(bodyParser.json());
+app.use(bodyParser.urlencoded({ extended: false }));
+app.use(cookieParser());
+app.use(cors());
+app.use(methodOverride());
+
+app.use('/v1/', api);
+app.use('/v1/states', states);
+app.use('/v1/regions', regions);
+app.use('/v1/cities', cities);
+
+// catch 404 and forward to error handler
+app.use((req, res, next) => {
+    res.status(404);
+    log.debug('%s %d %s', req.method, res.statusCode, req.url);
+    res.json({ error: 'Not found' }).end();
+});
+
+// error handlers
+app.use((err, req, res, next) => {
+    res.status(err.status || 500);
+    log.error('%s %d %s', req.method, res.statusCode, err.message);
+    res.json({ error: err.message }).end();
+});
+
+module.exports = app;
diff --git a/src/libs/config.js b/src/libs/config.js
new file mode 100644
index 00000000..ef5a26cd
--- /dev/null
+++ b/src/libs/config.js
@@ -0,0 +1,7 @@
+const nconf = require('nconf');
+
+nconf.argv()
+    .env()
+    .file({ file: `${process.cwd()}/config.json` });
+
+module.exports = nconf;
diff --git a/src/libs/db/monet.js b/src/libs/db/monet.js
new file mode 100644
index 00000000..1cf874cc
--- /dev/null
+++ b/src/libs/db/monet.js
@@ -0,0 +1,22 @@
+const MonetDBPool = require('monetdb-pool');
+
+const libs = `${process.cwd()}/libs`;
+
+const config = require(`${libs}/config`);
+
+const poolOptions = {
+    nrConnections: config.get('monetdb:nrConnections'),
+};
+
+const options = {
+    host: config.get('monetdb:host'),
+    port: config.get('monetdb:port'),
+    dbname: config.get('monetdb:dbname'),
+    user: config.get('monetdb:user'),
+    password: config.get('monetdb:password'),
+};
+
+const conn = new MonetDBPool(poolOptions, options);
+conn.connect();
+
+module.exports = conn;
diff --git a/src/libs/db/query_decorator.js b/src/libs/db/query_decorator.js
new file mode 100644
index 00000000..361f3509
--- /dev/null
+++ b/src/libs/db/query_decorator.js
@@ -0,0 +1,25 @@
+const libs = `${process.cwd()}/libs`;
+const log = require(`${libs}/log`)(module);
+const conn = require(`${libs}/db/monet`);
+
+/**
+ * Basic decorator to wrap SQL query strings
+ */
+exports.execQuery = (sqlQuery, sqlQueryParams) => {
+    log.debug(`Executing SQL query '${sqlQuery}' with params '${sqlQueryParams}'`);
+    conn.prepare(sqlQuery, true).then(
+        (dbQuery) => {
+            dbQuery.exec(sqlQueryParams).then(
+                (dbResult) => {
+                    log.debug(dbResult.data);
+                    log.debug(`Query result: ${dbResult.data}`);
+                    return dbResult;
+                },
+                (error) => {
+                    log.error(`SQL query execution error: ${error.message}`);
+                    return error;
+                }
+            );
+        }
+    );
+};
diff --git a/src/libs/enrollment/all.js b/src/libs/enrollment/all.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/libs/enrollment/city.js b/src/libs/enrollment/city.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/libs/enrollment/common.js b/src/libs/enrollment/common.js
new file mode 100644
index 00000000..207f44c3
--- /dev/null
+++ b/src/libs/enrollment/common.js
@@ -0,0 +1,14 @@
+const libs = `${process.cwd()}/libs`;
+
+const log = require(`${libs}/log`)(module);
+
+const sqlDecorator = require(`${libs}/query_decorator`);
+
+const yearRange = () => {
+    const yearSql = 'SELECT MIN(t.ano_censo) AS start_year, MAX(t.ano_censo)'
+        + 'AS end_year FROM turmas AS t';
+    log.debug('Generated SQL query for enrollments\' year range');
+    return sqlDecorator.execQuery(yearSql, []);
+};
+
+module.exports = yearRange;
diff --git a/src/libs/enrollment/country.js b/src/libs/enrollment/country.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/libs/enrollment/region.js b/src/libs/enrollment/region.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/libs/enrollment/state.js b/src/libs/enrollment/state.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/libs/log.js b/src/libs/log.js
new file mode 100644
index 00000000..0eb64119
--- /dev/null
+++ b/src/libs/log.js
@@ -0,0 +1,34 @@
+const winston = require('winston');
+
+winston.emitErrs = true;
+
+function getFilePath(module) {
+    // using filename in log statements
+    return module.filename.split('/').slice(-2).join('/');
+}
+
+function logger(module) {
+    return new winston.Logger({
+        transports: [
+            new winston.transports.File({
+                level: 'info',
+                filename: `${process.cwd()}/logs/all.log`,
+                handleException: true,
+                json: false,
+                maxSize: 5242880, // 5MB
+                maxFiles: 2,
+                colorize: false,
+            }),
+            new winston.transports.Console({
+                level: 'debug',
+                label: getFilePath(module),
+                handleException: true,
+                json: true,
+                colorize: true,
+            }),
+        ],
+        exitOnError: false,
+    });
+}
+
+module.exports = logger;
diff --git a/libs/routes/api.js b/src/libs/routes/api.js
similarity index 65%
rename from libs/routes/api.js
rename to src/libs/routes/api.js
index 9911bf10..d5c46ef6 100644
--- a/libs/routes/api.js
+++ b/src/libs/routes/api.js
@@ -1,20 +1,27 @@
-'use strict';
-
 const express = require('express');
+
 const xml = require('js2xmlparser');
-const enrollmentsApp = express();
 
-const libs = process.cwd() + '/libs/';
+const enrollmentApp = express();
+
+const libs = `${process.cwd()}/libs`;
 
-const log = require(libs + 'log')(module);
-const config = require(libs + 'config');
+const log = require(`${libs}/log`)(module);
 
-const conn = require(libs + 'db/monet');
+const conn = require(`${libs}/db/monet`);
+
+function response(req, res) {
+    if (req.query.format === 'csv') {
+        res.csv(req.result.data);
+    } else if (req.query.format === 'xml') {
+        res.send(xml('result', JSON.stringify({ state: req.result.data })));
+    } else {
+        res.json({ result: req.result.data });
+    }
+}
 
-enrollmentsApp.get('/', function (req, res) {
-    res.json({
-        msg: 'SimCAQ API is running'
-    });
+enrollmentApp.get('/', (req, res) => {
+    res.json({ msg: 'SimCAQ API is running' });
 });
 
 /**
@@ -22,69 +29,61 @@ enrollmentsApp.get('/', function (req, res) {
  *
  * Returns a tuple of start and ending years of the complete enrollments dataset.
  */
-enrollmentsApp.get('/year_range', function(req, res) {
-    var yearSql = "SELECT MIN(t.ano_censo) AS start_year, MAX(t.ano_censo) AS end_year FROM turmas AS t";
-    conn.query(yearSql, true).then(function(result) {
-      if (req.query.format === 'csv') {
-        res.csv(result.data);
-      } else if (req.query.format === 'xml') {
-        res.send(xml("result", JSON.stringify({year_range: result.data})))
-      }
-      else {
-        res.json({
-            result: result.data
-        });
-      }
-    }, function(error) {
-        log.error('SQL query error: ${ error.message }?');
-        log.debug(error);
-        res.send('Request could not be satisfied due to an internal error');
-    });
+enrollmentApp.get('/year_range', (req, res) => {
+    const yearSql = 'SELECT MIN(t.ano_censo) AS start_year, MAX(t.ano_censo)'
+        + 'AS end_year FROM turmas AS t';
+
+    conn.query(yearSql, true).then(
+        (dbResult) => {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        },
+        (dbError) => {
+            log.error(`SQL query execution error: ${dbError.message}`);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        }
+    );
 });
 
-enrollmentsApp.get('/education_level', function(req, res) {
-    var edLevelId = "SELECT pk_etapa_ensino_id AS id, desc_etapa AS education_level FROM etapa_ensino";
-    conn.query(edLevelId, true).then(function(result) {
-      if (req.query.format === 'csv') {
-        res.csv(result.data);
-      } else if (req.query.format === 'xml') {
-        res.send(xml("result", JSON.stringify({year_range: result.data})))
-      }
-      else {
-        res.json({
-            result: result.data
-        });
-      }
-    }, function(error) {
-        log.error('SQL query error: ${ error.message }?');
-        log.debug(error);
-        res.send('Request could not be satisfied due to an internal error');
-    });
+enrollmentApp.get('/education_level', (req, res) => {
+    const edLevelSql = 'SELECT pk_etapa_ensino_id AS id, desc_etapa AS '
+        + 'education_level FROM etapa_ensino';
+    conn.query(edLevelSql, true).then(
+        (dbResult) => {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        },
+        (dbError) => {
+            log.error(`SQL query error: ${dbError.message}`);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        }
+    );
 });
 
-enrollmentsApp.get('/data', function(req, res) {
-    log.debug(req.query)
-    log.debug(req.query.met)
-    log.debug(req.query.dim)
-    conn.query('SELECT * FROM turmas LIMIT 5', true).then(function(result) {
-      if (req.query.format === 'csv') {
-        res.csv(result.data);
-      } else if (req.query.format === 'xml') {
-        res.send(xml("result", JSON.stringify({data: result.data})))
-      }
-      else {
-        res.json({
-            result: result.data
-        });
-      }
-    }, function(error) {
-        log.error('SQL query error: ${ error.message }?');
-        log.debug(error);
-        res.send('Request could not be satisfied due to an internal error');
-    });
-})
+enrollmentApp.get('/data', (req, res) => {
+    log.debug(req.query);
+    log.debug(req.query.met);
+    log.debug(req.query.dim);
+    const schoolClassSql = 'SELECT * FROM turmas';
+    conn.query(schoolClassSql, true).then(
+        (dbResult) => {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        },
+        (dbError) => {
+            log.error(`SQL query error: ${dbError.message}`);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        }
+    );
+});
 
-enrollmentsApp.use('/enrollments', function(req, res, next) {
+enrollmentApp.use('/enrollments', (req, res, next) => {
     const params = req.query;
     req.paramCnt = 0;
 
@@ -116,7 +115,7 @@ enrollmentsApp.use('/enrollments', function(req, res, next) {
     next();
 });
 
-enrollmentsApp.use('/enrollments', function(req, res, next) {
+enrollmentApp.use('/enrollments', (req, res, next) => {
     const params = req.query;
     if (typeof params.aggregate !== 'undefined' && params.aggregate === 'region') {
         log.debug('Using enrollments query for regions');
@@ -163,7 +162,7 @@ enrollmentsApp.use('/enrollments', function(req, res, next) {
     next();
 });
 
-enrollmentsApp.use('/enrollments', function(req, res, next) {
+enrollmentApp.use('/enrollments', (req, res, next) => {
     const params = req.query;
     if (typeof params.aggregate !== 'undefined' && params.aggregate === 'state') {
         log.debug('Using enrollments query for states');
@@ -201,8 +200,8 @@ enrollmentsApp.use('/enrollments', function(req, res, next) {
         req.sqlQuery += ')';
 
         if (typeof req.id !== 'undefined') {
-            req.sqlQuery += " WHERE ";
-            req.sqlQuery += "e.pk_estado_id = ?";
+            req.sqlQuery += ' WHERE ';
+            req.sqlQuery += 'e.pk_estado_id = ?';
             req.sqlQueryParams.push(req.id);
         }
 
@@ -211,7 +210,7 @@ enrollmentsApp.use('/enrollments', function(req, res, next) {
     next();
 });
 
-enrollmentsApp.use('/enrollments', function(req, res, next) {
+enrollmentApp.use('/enrollments', (req, res, next) => {
     const params = req.query;
     if (typeof params.aggregate !== 'undefined' && params.aggregate === 'city') {
         log.debug('Using enrollments query for cities');
@@ -248,8 +247,8 @@ enrollmentsApp.use('/enrollments', function(req, res, next) {
         req.sqlQuery += ')';
 
         if (typeof req.id !== 'undefined') {
-            req.sqlQuery += " WHERE ";
-            req.sqlQuery += "m.pk_municipio_id = ?";
+            req.sqlQuery += ' WHERE ';
+            req.sqlQuery += 'm.pk_municipio_id = ?';
             req.sqlQueryParams.push(req.id);
         }
 
@@ -258,7 +257,7 @@ enrollmentsApp.use('/enrollments', function(req, res, next) {
     next();
 });
 
-enrollmentsApp.use('/enrollments', function(req, res, next) {
+enrollmentApp.use('/enrollments', (req, res, next) => {
     const params = req.query;
     if (typeof params.aggregate === 'undefined') {
         log.debug('Using enrollments query for the whole country');
@@ -302,35 +301,33 @@ enrollmentsApp.use('/enrollments', function(req, res, next) {
     next();
 });
 
-enrollmentsApp.get('/enrollments', function(req, res, next) {
-    log.debug('Request parameters: ${ req }?');
+enrollmentApp.get('/enrollments', (req, res, next) => {
+    log.debug(`Request parameters: ${req}`);
     if (typeof req.sqlQuery === 'undefined') {
-        /* Should only happen if there is a bug in the chaining of the
-         * '/enrollments' route, since when no +aggregate+ parameter is given,
-         * it defaults to use the query for the whole country.
-         */
+        // Should only happen if there is a bug in the chaining of the
+        // '/enrollments' route, since when no +aggregate+ parameter is given,
+        // it defaults to use the query for the whole country.
         log.error('BUG -- No SQL query was found to be executed!');
         res.send('Request could not be satisfied due to an internal error');
     } else {
         log.debug('SQL query: ${ req.sqlQuery }?');
         log.debug(req.sqlQuery);
-        conn.prepare(req.sqlQuery, true).then(function(dbQuery) {
-            dbQuery.exec(req.sqlQueryParams).then(function(dbResult) {
-                log.debug(dbResult);
-                if (req.query.format === 'csv') {
-                    res.csv(dbResult.data);
-                } else if (req.query.format === 'xml') {
-                    res.send(xml('result', JSON.stringify({enrollments: dbResult.data})));
-                } else {
-                    res.json({ result: dbResult.data });
+
+        conn.prepare(req.sqlQuery, true).then((dbQuery) => {
+            dbQuery.exec(req.sqlQueryParams).then(
+                (dbResult) => {
+                    log.debug(dbResult);
+                    req.result = dbResult;
+                    response(req, res);
+                },
+                (dbError) => {
+                    log.error(`SQL query execution error: ${dbError.message}`);
+                    // FIXME: change response to HTTP 501 status
+                    res.json({ error: 'An internal error has occurred' }).end();
                 }
-            });
-        }, function(error) {
-                log.error('SQL query error: ${ error.message }?');
-                log.debug(error);
-                res.send('Request could not be satisfied due to an internal error');
+            );
         });
     }
 });
 
-module.exports = enrollmentsApp
+module.exports = enrollmentApp;
diff --git a/src/libs/routes/cities.js b/src/libs/routes/cities.js
new file mode 100644
index 00000000..1ae6c48d
--- /dev/null
+++ b/src/libs/routes/cities.js
@@ -0,0 +1,95 @@
+const express = require('express');
+
+const xml = require('js2xmlparser');
+
+const cityApp = express();
+
+const libs = `${process.cwd()}/libs`;
+
+const log = require(`${libs}/log`)(module);
+
+const conn = require(`${libs}/db/monet`);
+
+function response(req, res) {
+    if (req.query.format === 'csv') {
+        res.csv(req.result.data);
+    } else if (req.query.format === 'xml') {
+        res.send(xml('result', JSON.stringify({ city: req.result.data })));
+    } else {
+        res.json({ result: req.result.data });
+    }
+}
+
+cityApp.get('/', (req, res) => {
+    conn.query('SELECT * FROM municipios', true).then(
+        (dbResult) => {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        },
+        (dbError) => {
+            log.error(`SQL query execution error: ${dbError.message}`);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        }
+    );
+});
+
+cityApp.get('/:id', (req, res) => {
+    const citySql = 'SELECT * FROM municipios WHERE pk_municipio_id = ?';
+    const cityId = parseInt(req.params.id, 10);
+    conn.prepare(citySql, true).then((dbQuery) => {
+        dbQuery.exec([cityId]).then(
+            (dbResult) => {
+                log.debug(dbResult);
+                req.result = dbResult;
+                response(req, res);
+            },
+            (dbError) => {
+                log.error(`SQL query execution error: ${dbError.message}`);
+                // FIXME: change response to HTTP 501 status
+                res.json({ error: 'An internal error has occurred' }).end();
+            }
+        );
+    });
+});
+
+cityApp.get('/ibge/:id', (req, res) => {
+    const citySql = 'SELECT * FROM municipios WHERE codigo_ibge = ?';
+    const cityId = parseInt(req.params.id, 10);
+    conn.prepare(citySql, true).then((dbQuery) => {
+        dbQuery.exec([cityId]).then(
+            (dbResult) => {
+                log.debug(dbResult);
+                req.result = dbResult;
+                response(req, res);
+            },
+            (dbError) => {
+                log.error(`SQL query execution error: ${dbError.message}`);
+                // FIXME: change response to HTTP 501 status
+                res.json({ error: 'An internal error has occurred' }).end();
+            }
+        );
+    });
+});
+
+cityApp.get('/state/:id', (req, res) => {
+    const citySql = 'SELECT * FROM municipios WHERE fk_estado_id = ?';
+    const stateId = parseInt(req.params.id, 10);
+    conn.prepare(citySql, true).then((dbQuery) => {
+        dbQuery.exec([stateId]).then(
+            (dbResult) => {
+                log.debug(dbResult);
+                req.result = dbResult;
+                response(req, res);
+            },
+            (dbError) => {
+                log.error(`SQL query execution error: ${dbError.message}`);
+                // FIXME: change response to HTTP 501 status
+                res.json({ error: 'An internal error has occurred' }).end();
+            }
+        );
+    });
+});
+
+module.exports = cityApp;
diff --git a/src/libs/routes/regions.js b/src/libs/routes/regions.js
new file mode 100644
index 00000000..d76037c0
--- /dev/null
+++ b/src/libs/routes/regions.js
@@ -0,0 +1,58 @@
+const express = require('express');
+
+const xml = require('js2xmlparser');
+
+const regionApp = express();
+
+const libs = `${process.cwd()}/libs`;
+
+const log = require(`${libs}/log`)(module);
+
+const conn = require(`${libs}/db/monet`);
+
+function response(req, res) {
+    if (req.query.format === 'csv') {
+        res.csv(req.result.data);
+    } else if (req.query.format === 'xml') {
+        res.send(xml('result', JSON.stringify({ state: req.result.data })));
+    } else {
+        res.json({ result: req.result.data });
+    }
+}
+
+regionApp.get('/', (req, res) => {
+    const regionSql = 'SELECT * FROM regioes';
+    conn.query(regionSql, true).then(
+        (dbResult) => {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        },
+        (dbError) => {
+            log.error(`SQL query execution error: ${dbError.message}`);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        }
+    );
+});
+
+regionApp.get('/:id', (req, res) => {
+    const regionSql = 'SELECT * FROM regioes WHERE pk_regiao_id = ?';
+    const regionId = parseInt(req.params.id, 10);
+    conn.prepare(regionSql, true).then((dbQuery) => {
+        dbQuery.exec([regionId]).then(
+            (dbResult) => {
+                log.debug(dbResult);
+                req.result = dbResult;
+                response(req, res);
+            },
+            (dbError) => {
+                log.error(`SQL query execution error: ${dbError.message}`);
+                // FIXME: change response to HTTP 501 status
+                res.json({ error: 'An internal error has occurred' }).end();
+            }
+        );
+    });
+});
+
+module.exports = regionApp;
diff --git a/src/libs/routes/states.js b/src/libs/routes/states.js
new file mode 100644
index 00000000..23cc7a3d
--- /dev/null
+++ b/src/libs/routes/states.js
@@ -0,0 +1,77 @@
+const express = require('express');
+
+const xml = require('js2xmlparser');
+
+const stateApp = express();
+
+const libs = `${process.cwd()}/libs`;
+
+const log = require(`${libs}/log`)(module);
+
+const conn = require(`${libs}/db/monet`);
+
+function response(req, res) {
+    if (req.query.format === 'csv') {
+        res.csv(req.result.data);
+    } else if (req.query.format === 'xml') {
+        res.send(xml('result', JSON.stringify({ state: req.result.data })));
+    } else {
+        res.json({ result: req.result.data });
+    }
+}
+
+stateApp.get('/', (req, res, next) => {
+    const stateSql = 'SELECT * FROM estados';
+    conn.query(stateSql, true).then(
+        (dbResult) => {
+            log.debug(dbResult);
+            req.result = dbResult;
+            response(req, res);
+        },
+        (dbError) => {
+            log.error(`SQL query execution error: ${dbError.message}`);
+            // FIXME: change response to HTTP 501 status
+            res.json({ error: 'An internal error has occurred' }).end();
+        }
+    );
+});
+
+stateApp.get('/:id', (req, res, next) => {
+    const stateSql = 'SELECT * FROM estados WHERE pk_estado_id = ?';
+    const stateId = parseInt(req.params.id, 10);
+    conn.prepare(stateSql, true).then((dbQuery) => {
+        dbQuery.exec([stateId]).then(
+            (dbResult) => {
+                log.debug(dbResult);
+                req.result = dbResult;
+                response(req, res);
+            },
+            (dbError) => {
+                log.error(`SQL query execution error: ${dbError.message}`);
+                // FIXME: change response to HTTP 501 status
+                res.json({ error: 'An internal error has occurred' }).end();
+            }
+        );
+    });
+});
+
+stateApp.get('/region/:id', (req, res, next) => {
+    const stateSql = 'SELECT * FROM estados WHERE fk_regiao_id = ?';
+    const regionId = parseInt(req.params.id, 10);
+    conn.prepare(stateSql, true).then((dbQuery) => {
+        dbQuery.exec([regionId]).then(
+            (dbResult) => {
+                log.debug(dbResult);
+                req.result = dbResult;
+                response(req, res);
+            },
+            (dbError) => {
+                log.error(`SQL query execution error: ${dbError.message}`);
+                // FIXME: change response to HTTP 501 status
+                res.json({ error: 'An internal error has occurred' }).end();
+            }
+        );
+    });
+});
+
+module.exports = stateApp;
diff --git a/src/server.js b/src/server.js
new file mode 100644
index 00000000..7b3927e3
--- /dev/null
+++ b/src/server.js
@@ -0,0 +1,16 @@
+const debug = require('debug')('node-express-base');
+
+const libs = `${process.cwd()}/libs`;
+
+const config = require(`${libs}/config`);
+
+const log = require(`${libs}/log`)(module);
+
+const app = require(`${libs}/app`);
+
+app.set('port', config.get('port') || 3000);
+
+const server = app.listen(app.get('port'), () => {
+    debug(`Express server listening on port ${server.address().port}`);
+    log.info(`Express server listening on port ${config.get('port')}`);
+});
diff --git a/src/test/test.js b/src/test/test.js
new file mode 100644
index 00000000..f9fec313
--- /dev/null
+++ b/src/test/test.js
@@ -0,0 +1,155 @@
+const chai = require('chai');
+const chaiHttp = require('chai-http');
+const assert = chai.assert;
+const expect = chai.expect;
+const should = chai.should(); // actually call the function
+const server = require('../libs/app');
+
+chai.use(chaiHttp);
+
+describe('request enrollments', () => {
+    it('should list enrollments', (done) => {
+        chai.request(server)
+            .get('/v1/enrollments')
+            .end((err, res) => {
+                res.should.have.status(200);
+                res.should.be.json;
+                res.body.should.have.property('result');
+                res.body.result.should.be.a('array');
+                res.body.result[0].should.have.property('name');
+                res.body.result[0].should.have.property('total');
+                done();
+            });
+    });
+});
+
+describe('request regions', () => {
+    it('should list all regions', (done) => {
+        chai.request(server)
+            .get('/v1/regions')
+            .end((err, res) => {
+                res.should.have.status(200);
+                res.should.be.json;
+                res.body.should.have.property('result');
+                res.body.result.should.be.a('array');
+                res.body.result[0].should.have.property('pk_regiao_id');
+                res.body.result[0].should.have.property('nome');
+                done();
+            });
+    });
+
+    it('should list region by id', (done) => {
+        chai.request(server)
+            .get('/v1/regions/1')
+            .end((err, res) => {
+                res.should.have.status(200);
+                res.should.be.json;
+                res.body.should.have.property('result');
+                res.body.result.should.be.a('array');
+                res.body.result.should.have.length(1);
+                res.body.result[0].should.have.property('pk_regiao_id');
+                res.body.result[0].should.have.property('nome');
+                done();
+            });
+    });
+});
+
+describe('request states', () => {
+
+    it('should list all states', (done) => {
+        chai.request(server)
+            .get('/v1/states')
+            .end((err, res) => {
+                res.should.have.status(200);
+                res.should.be.json;
+                res.body.should.have.property('result');
+                res.body.result.should.be.a('array');
+                res.body.result[0].should.have.property('pk_estado_id');
+                res.body.result[0].should.have.property('fk_regiao_id');
+                res.body.result[0].should.have.property('nome');
+                done();
+            });
+    });
+
+    it('should list a state by id', (done) => {
+        chai.request(server)
+            .get('/v1/states/11')
+            .end((err, res) => {
+                res.should.have.status(200);
+                res.should.be.json;
+                res.body.should.have.property('result');
+                res.body.result.should.be.a('array');
+                res.body.result.should.have.length(1);
+                res.body.result[0].should.have.property('pk_estado_id');
+                res.body.result[0].should.have.property('fk_regiao_id');
+                res.body.result[0].should.have.property('nome');
+                done();
+            });
+    });
+
+    it('should list states by region id', (done) => {
+        chai.request(server)
+            .get('/v1/states/region/1')
+            .end((err, res) => {
+                res.should.have.status(200);
+                res.should.be.json;
+                res.body.should.have.property('result');
+                res.body.result.should.be.a('array');
+                res.body.result[0].should.have.property('pk_estado_id');
+                res.body.result[0].should.have.property('fk_regiao_id');
+                res.body.result[0].should.have.property('nome');
+                done();
+            });
+    });
+});
+
+describe('request cities', () => {
+
+    it('should list all cities', (done) => {
+        chai.request(server)
+            .get('/v1/cities')
+            .end((err, res) => {
+                res.should.have.status(200);
+                res.should.be.json;
+                res.body.should.have.property('result');
+                res.body.result.should.be.a('array');
+                res.body.result[0].should.have.property('pk_municipio_id');
+                res.body.result[0].should.have.property('fk_estado_id');
+                res.body.result[0].should.have.property('nome');
+                res.body.result[0].should.have.property('codigo_ibge');
+                done();
+            });
+    });
+
+    it('should list a city by id', (done) => {
+        chai.request(server)
+            .get('/v1/cities/1')
+            .end((err, res) => {
+                res.should.have.status(200);
+                res.should.be.json;
+                res.body.should.have.property('result');
+                res.body.result.should.be.a('array');
+                res.body.result[0].should.have.property('pk_municipio_id');
+                res.body.result[0].should.have.property('fk_estado_id');
+                res.body.result[0].should.have.property('nome');
+                res.body.result[0].should.have.property('codigo_ibge');
+                done();
+            });
+    });
+
+    it('should list a city by codigo_ibge', (done) => {
+        chai.request(server)
+            .get('/v1/cities/ibge/1200013')
+            .end((err, res) => {
+                res.should.have.status(200);
+                res.should.be.json;
+                res.body.should.have.property('result');
+                res.body.result.should.be.a('array');
+                res.body.result[0].should.have.property('pk_municipio_id');
+                res.body.result[0].should.have.property('fk_estado_id');
+                res.body.result[0].should.have.property('nome');
+                res.body.result[0].should.have.property('codigo_ibge');
+                done();
+            });
+    });
+});
diff --git a/test/test.js b/test/test.js
deleted file mode 100644
index a9b67bb2..00000000
--- a/test/test.js
+++ /dev/null
@@ -1,158 +0,0 @@
-var chai = require('chai');
-var chaiHttp = require('chai-http');
-var assert = chai.assert;
-var expect = chai.expect;
-var should = chai.should(); //actually call the function
-var server = require('../libs/app');
-
-chai.use(chaiHttp);
-
-describe('request enrollments', function(){
-
-  it('should list enrollments', function(done){
-    chai.request(server)
-      .get('/v1/enrollments')
-      .end(function(err, res){
-        res.should.have.status(200);
-        res.should.be.json;
-        res.body.should.have.property('result');
-        res.body.result.should.be.a('array');
-        res.body.result[0].should.have.property('name');
-        res.body.result[0].should.have.property('total');
-        done();
-      })
-  });
-});
-
-describe('request regions', function(){
-
-  it('should list all regions', function(done){
-    chai.request(server)
-      .get('/v1/regions')
-      .end(function(err, res){
-        res.should.have.status(200);
-        res.should.be.json;
-        res.body.should.have.property('result');
-        res.body.result.should.be.a('array');
-        res.body.result[0].should.have.property('pk_regiao_id');
-        res.body.result[0].should.have.property('nome');
-        done();
-      })
-  });
-
-  it('should list region by id', function(done){
-    chai.request(server)
-      .get('/v1/regions/1')
-      .end(function(err, res){
-        res.should.have.status(200);
-        res.should.be.json;
-        res.body.should.have.property('result');
-        res.body.result.should.be.a('array');
-        res.body.result.should.have.length(1);
-        res.body.result[0].should.have.property('pk_regiao_id');
-        res.body.result[0].should.have.property('nome');
-        done();
-      })
-  });
-});
-
-describe('request states', function(){
-
-  it('should list all states', function(done){
-    chai.request(server)
-      .get('/v1/states')
-      .end(function(err, res){
-        res.should.have.status(200);
-        res.should.be.json;
-        res.body.should.have.property('result');
-        res.body.result.should.be.a('array');
-        res.body.result[0].should.have.property('pk_estado_id');
-        res.body.result[0].should.have.property('fk_regiao_id');
-        res.body.result[0].should.have.property('nome');
-        done();
-      })
-  });
-
-  it('should list a state by id', function(done){
-    chai.request(server)
-      .get('/v1/states/11')
-      .end(function(err, res){
-        res.should.have.status(200);
-        res.should.be.json;
-        res.body.should.have.property('result');
-        res.body.result.should.be.a('array');
-        res.body.result.should.have.length(1);
-        res.body.result[0].should.have.property('pk_estado_id');
-        res.body.result[0].should.have.property('fk_regiao_id');
-        res.body.result[0].should.have.property('nome');
-        done();
-      })
-  });
-
-  it('should list states by region id', function(done){
-    chai.request(server)
-      .get('/v1/states/region/1')
-      .end(function(err, res){
-        res.should.have.status(200);
-        res.should.be.json;
-        res.body.should.have.property('result');
-        res.body.result.should.be.a('array');
-        res.body.result[0].should.have.property('pk_estado_id');
-        res.body.result[0].should.have.property('fk_regiao_id');
-        res.body.result[0].should.have.property('nome');
-        done();
-      })
-  });
-});
-
-describe('request cities', function(){
-
-  it('should list all cities', function(done){
-    chai.request(server)
-      .get('/v1/cities')
-      .end(function(err, res){
-        res.should.have.status(200);
-        res.should.be.json;
-        res.body.should.have.property('result');
-        res.body.result.should.be.a('array');
-        res.body.result[0].should.have.property('pk_municipio_id');
-        res.body.result[0].should.have.property('fk_estado_id');
-        res.body.result[0].should.have.property('nome');
-        res.body.result[0].should.have.property('codigo_ibge');
-        done();
-      })
-  });
-
-  it('should list a city by id', function(done){
-    chai.request(server)
-      .get('/v1/cities/1')
-      .end(function(err, res){
-        res.should.have.status(200);
-        res.should.be.json;
-        res.body.should.have.property('result');
-        res.body.result.should.be.a('array');
-        res.body.result[0].should.have.property('pk_municipio_id');
-        res.body.result[0].should.have.property('fk_estado_id');
-        res.body.result[0].should.have.property('nome');
-        res.body.result[0].should.have.property('codigo_ibge');
-        done();
-      })
-  });
-
-  it('should list a city by codigo_ibge', function(done){
-    chai.request(server)
-      .get('/v1/cities/ibge/1200013')
-      .end(function(err, res){
-        res.should.have.status(200);
-        res.should.be.json;
-        res.body.should.have.property('result');
-        res.body.result.should.be.a('array');
-        res.body.result[0].should.have.property('pk_municipio_id');
-        res.body.result[0].should.have.property('fk_estado_id');
-        res.body.result[0].should.have.property('nome');
-        res.body.result[0].should.have.property('codigo_ibge');
-        done();
-      })
-  });
-
-});
-- 
GitLab