How to create a modular RESTfull API with Node.js, Express and ECMAScript 6 Part 2

This is part 2 of building a RESTful API with Node.js, Express and ECMAScript 6 using a module approach. Be sure to read part 1 first.

Go to https://github.com/LuukMoret/blog-node-api-express-es6-part2 to see the finished code.

Now that the project setup is done we can start writing our own modules.

Create dummies module

Create dummies folder inside the app folder

mkdir app/dummies

Create controller file dummies.controller.js inside app/dummies, this will handle the routing of the module. Add the following content:

'use strict';
const express = require('express');
const router = express.Router();
const dummiesService = require('./dummies.service');
router.get('/dummies', (req, res) => {
dummiesService.getDummies()
.then((response) => {
res.send(response);
})
.catch((response) => {
res.status(response.statusCode).send(response.error);
});
});
module.exports = router;

Update middleware/routes.js file by adding the newly created route. Replace the file with the following content:

'use strict';
const winston = require('winston'); // https://www.npmjs.com/package/winston
const dummiesRoute = require('../app/dummies/dummies.controller');
exports.register = (app) => {
app.use(dummiesRoute);
winston.info('app - routes: dummies loaded');
};

Create service file dummies.service.js inside app/dummies, this will contain the (business) logic of the module. Add the following content:

'use strict';
const dummiesRepository = require('./dummies.repository');
const Dummies = require('./dummies.model');
exports.getDummies = () => {
return new Promise((resolve, reject) => {
dummiesRepository.getDummiesData()
.then((response) => {
// business logic
const dummies = response.map((dummies) => new Dummies(dummies));
resolve(dummies);
})
.catch((response) => {
reject(response);
});
});
};

Create model file dummies.model.js inside app/dummies, this will handle data transformation. Add the following content:

'use strict';
class Dummies {
constructor(dummiesResponse) {
this.id = dummiesResponse.id;
this.name = dummiesResponse.login;
}
}
module.exports = Dummies;

Create repository file dummies.repository.js inside app/dummies, this will handle the repository calls (in this case git api call for demo purposes). Add the following content:

'use strict';
const rp = require('request-promise');
exports.getDummiesData = () => {
const options = {
uri: 'https://api.github.com/users',
headers: {
'User-Agent': 'Request-Promise'
},
json: true // Automatically parses the JSON string in the response
};
return rp(options)
.then((response) => {
return response;
})
.catch((response) => {
throw response;
});
};

We can test the application now. Run npm start and navigate with a browser to http://localhost:3100/dummies and you should see a json response from the node application.

json response

Now that we have written our dummies module it is time to write some unit and integration tests.

Create controller spec file dummies.controller.spec.js inside app/dummies with the following content:

'use strict';
const app = require('../../app');
const server = app.listen();
const winston = require('winston');
const supertest = require('supertest').agent(server);
const sinon = require('sinon');
require('sinon-as-promised');
const dummiesService = require('./dummies.service');
let dummiesServiceStub;
before(() => {
try {
winston.remove(winston.transports.Console);
} catch (error) {
}
});
after(() => {
server.close();
});
afterEach(() => {
if (dummiesServiceStub) {
dummiesServiceStub.restore();
}
});
describe('unit: dummies.controller - when getting dummies', () => {
it('it should resolve and return dummies', () => {
// arrange
const dummies = [
{id: 1, login: 'dummy1'},
{id: 2, login: 'dummy2'},
{id: 3, login: 'dummy3'}
];
dummiesServiceStub = sinon
.stub(dummiesService, 'getDummies')
.resolves(dummies);
// act + assert
return supertest
.get('/dummies')
.expect(200, dummies);
});
it('it should reject and return an error message', () => {
// arrange
dummiesServiceStub = sinon
.stub(dummiesService, 'getDummies')
.rejects({statusCode: 500, error: {error: 'error'}});
// act + assert
return supertest
.get('/dummies')
.expect(500, {error: 'error'});
});
});

Create service spec file dummies.service.spec.js inside app/dummies with the following content:

'use strict';
const chai = require('chai');
const expect = chai.expect;
const sinon = require('sinon');
require('sinon-as-promised');
const dummiesService = require('./dummies.service');
const dummiesRepository = require('./dummies.repository');
let dummiesRepositoryStub;
afterEach(() => {
if (dummiesRepositoryStub) {
dummiesRepositoryStub.restore();
}
});
describe('unit: dummies.service - when getting dummies data', () => {
it('it should resolve and return dummies data', () => {
// arrange
const dummiesResponse = [
{id: 1, login: 'dummy1', additionalData: 'data'},
{id: 2, login: 'dummy2', additionalData: 'data'},
{id: 3, login: 'dummy3', additionalData: 'data'}
];
const dummiesTransformedModel = [
{id: 1, name: 'dummy1'},
{id: 2, name: 'dummy2'},
{id: 3, name: 'dummy3'}
];
dummiesRepositoryStub = sinon
.stub(dummiesRepository, 'getDummiesData')
.resolves(dummiesResponse);
// act + assert
return dummiesService.getDummies()
.then((response) => {
expect(dummiesRepositoryStub.called).to.equal(true);
expect(response).to.deep.equal(dummiesTransformedModel);
});
});
it('it should reject the request and return an error message', () => {
// arrange
dummiesRepositoryStub = sinon
.stub(dummiesRepository, 'getDummiesData')
.rejects({error: 'error'});
// act + assert
return dummiesService.getDummies()
.catch((response) => {
expect(dummiesRepositoryStub.called).to.equal(true);
expect(response).to.deep.equal({error: 'error'});
});
});
});

Create repository spec file dummies.repository.spec.js inside app/dummies with the following content:

'use strict';
const chai = require('chai');
const expect = chai.expect;
const nock = require('nock');
const dummiesRepository = require('./dummies.repository');
afterEach(() => {
nock.cleanAll();
});
describe('unit: dummies.repository - when getting dummies data', () => {
it('it should resolve and return dummies data', () => {
// arrange
const dummiesResponse = [
{id: 1, login: 'dummy1', additionalData: 'data'},
{id: 2, login: 'dummy2', additionalData: 'data'},
{id: 3, login: 'dummy3', additionalData: 'data'}
];
nock('https://api.github.com')
.get('/users')
.reply(200, dummiesResponse);
// act + assert
return dummiesRepository.getDummiesData()
.then((response) => {
expect(response).to.deep.equal(dummiesResponse);
});
});
it('it should reject and return an error message', () => {
// arrange
nock('https://api.github.com')
.get('/users')
.replyWithError({'message': 'something awful happened', 'code': 'AWFUL_ERROR'});
// act + assert
return dummiesRepository.getDummiesData()
.catch((response) => {
expect(response.error).to.deep.equal({'message': 'something awful happened', 'code': 'AWFUL_ERROR'});
});
});
});

Create integration file dummies.integration.js inside app/dummies with the following content:

'use strict';
const app = require('../../bin/www');
const server = app.listen();
const supertest = require('supertest').agent(server);
const winston = require('winston');
const nock = require('nock');
before(() => {
try {
winston.remove(winston.transports.Console);
} catch (error) {
}
});
after(() => {
server.close();
});
afterEach(() => {
nock.cleanAll();
});
describe('integration: dummies.integration.js - when getting dummies', () => {
const dummiesResponse = [
{id: 1, login: 'dummy'},
{id: 2, login: 'dummy2'}
];
const dummiesTransformationModel = [
{id: 1, name: 'dummy'},
{id: 2, name: 'dummy2'}
];
it('it should return dummies', () => {
nock('https://api.github.com')
.get('/users')
.reply(200, dummiesResponse);
return supertest
.get('/dummies')
.expect(200, dummiesTransformationModel);
});
it('it should return not found', () => {
nock('https://api.github.com')
.get('/users')
.reply(404);
return supertest
.get('/dummies')
.expect(404);
});
it('it should return error', () => {
nock('https://api.github.com')
.get('/users')
.reply(500, {'message': 'something awful happened', 'code': 'AWFUL_ERROR'});
return supertest
.get('/dummies')
.expect(500, {'message': 'something awful happened', 'code': 'AWFUL_ERROR'});
});
});

We are now able to run npm test and all unit tests and integration tests will kick off.

A nice coverage report for the unit and integration tests will be generate in de /coverage folder.

coverage overview coverage file

That’s it for this blog post! I will try to create a Yeoman template for this in the future. Feel free to leave feedback and comments or have any questions about this.

Share Comments