diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000000000000000000000000000000000000..834a48a40945a182db82602de135718f81d471b6
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1 @@
+{ 'presets': ['es2015'] }
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000000000000000000000000000000000000..52b136d7a6031bf708bec43b27c56478555241b3
--- /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 1be0326ac9a0235b55fa03f244f261b16eed1832..f4e4627c8c4135f6a6d064b016e457de3042aae0 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 3171c9f7e5653d1e4edbfd1064779609693773c3..a3c44c8f571f3fe61cc8cd42e07b6159a8c47cea 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 0000000000000000000000000000000000000000..c919cf1c932e57792896a210082c83386e193a6d
--- /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 0000000000000000000000000000000000000000..a678b1c48f2fc93277fa47d2aff07c1fdf3aa659
--- /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 0000000000000000000000000000000000000000..2f7bd18e7d52121f1f78d197bc31d79cebd0bb25
--- /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 0000000000000000000000000000000000000000..c8d29099ae9f4802b09ac38136583ecc979aa2df
--- /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 0000000000000000000000000000000000000000..ce0697d756a4f0f7fba74542889c47976fc5dc9a
--- /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 0000000000000000000000000000000000000000..9a390c31f71bc7eae1522a280a2dc8f6723185bf
--- /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 0000000000000000000000000000000000000000..9a390c31f71bc7eae1522a280a2dc8f6723185bf
--- /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 0000000000000000000000000000000000000000..9c78895181d5b81053b58d89d2bc2d3c93340e02
--- /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 0000000000000000000000000000000000000000..9a390c31f71bc7eae1522a280a2dc8f6723185bf
--- /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 0000000000000000000000000000000000000000..9a390c31f71bc7eae1522a280a2dc8f6723185bf
--- /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 0000000000000000000000000000000000000000..9a390c31f71bc7eae1522a280a2dc8f6723185bf
--- /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 0000000000000000000000000000000000000000..e93d2ff2b7a0a81141b85099b8b97835a74159b3
--- /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 0000000000000000000000000000000000000000..ce0697d756a4f0f7fba74542889c47976fc5dc9a
--- /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 0000000000000000000000000000000000000000..812eae438319d38aaabeeb2a15cf0f7aa02953ea
--- /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 0000000000000000000000000000000000000000..f549624eb0908065775f216ef472fa45c705617d
--- /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 0000000000000000000000000000000000000000..75cf2bee0d1923460e3899b81c4fbdfcd5ae6d57
--- /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 0000000000000000000000000000000000000000..8579f3929b4ba24ff1308f1126706eb70874c339
--- /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 0000000000000000000000000000000000000000..397b4a7624e35fa60563a9c03b1213d93f7b6546
--- /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 0000000000000000000000000000000000000000..5f2171cc5b90c1149027cd7df7e6ec464de483c7
--- /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 0000000000000000000000000000000000000000..55caa55c10bdd703997e6355a6fb432ebb66cd1a
--- /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 8f60036adf20a0512e709e683f876de75124d714..1da8951e6da701180969231dfa6151489e2794f9 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 0000000000000000000000000000000000000000..e07481f9eecc0b8219d5355a994eaf2ee1d56d74
--- /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 544f649b66c258f98cad7ef867217c2a159dfae6..0000000000000000000000000000000000000000
--- 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 78f7831a7b1bb614fcebd8de04be18ff447d84a1..0000000000000000000000000000000000000000
--- 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 0b554285d1b9d889ca1f40f7607672490eda73b3..0000000000000000000000000000000000000000
--- 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 419b3e4b86d5d1e5a8e17858882c905204bab079..0000000000000000000000000000000000000000
--- 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 6a2220497fa90a47200fce15b0879c8cd5ed90c8..0000000000000000000000000000000000000000
--- 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 6c85fccb8a8b856769ccb46ca9fb821c81bab460..0000000000000000000000000000000000000000
--- 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 2192c5377a0e705ad59393dacf46a469491b7ad7..0000000000000000000000000000000000000000
--- 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 fd0765bb333bc1fa382aae1e82ecb4f500774df4..cee6b8e062bde5c7b96d92adcc9235ab99352ea7 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 dc0c1ce36563fd3fb4d967480e54548b7c9fff4c..0000000000000000000000000000000000000000
--- 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 0000000000000000000000000000000000000000..c239642062506c72c801dd6d3140fd7ea138cd3a
--- /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 0000000000000000000000000000000000000000..ef5a26cddbb3946b89ac690e8cf52d28884463aa
--- /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 0000000000000000000000000000000000000000..1cf874cc9ca5a2ada3e20bdd077c34fbcfbe8226
--- /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 0000000000000000000000000000000000000000..361f350978622de154aa8aebfa6143cddcf23127
--- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/libs/enrollment/city.js b/src/libs/enrollment/city.js
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/libs/enrollment/common.js b/src/libs/enrollment/common.js
new file mode 100644
index 0000000000000000000000000000000000000000..207f44c35f315bdd9cda4ed85b7cc8d8e8d687b3
--- /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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/libs/enrollment/region.js b/src/libs/enrollment/region.js
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/libs/enrollment/state.js b/src/libs/enrollment/state.js
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/src/libs/log.js b/src/libs/log.js
new file mode 100644
index 0000000000000000000000000000000000000000..0eb64119f54d8de753bb2d2cd01390f6dd48f20b
--- /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 9911bf10c84a45c3f661be4bcd5fce399f3cff04..d5c46ef65c7914eb5bb63f5242933c1b9233ff60 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 0000000000000000000000000000000000000000..1ae6c48d8d2a71232c1d71e8f8875de0dd13770e
--- /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 0000000000000000000000000000000000000000..d76037c08049be5e7b500ae6cb13cc8acec7d82a
--- /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 0000000000000000000000000000000000000000..23cc7a3dd01a1911e55d02c263739da789338138
--- /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 0000000000000000000000000000000000000000..7b3927e3fc64fe9f0001f01e95b0a2805feb33eb
--- /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 0000000000000000000000000000000000000000..f9fec3136184373433aaab391825b1f66e7c5aab
--- /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 a9b67bb2269983afbbe479cd72fea744330e81da..0000000000000000000000000000000000000000
--- 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();
-      })
-  });
-
-});