Skip to content

Commit 300c279

Browse files
committed
Add support for API versioning
- Refactor Route to support multiple handlers and api versioning. - Add HeaderApiVersioning and RouteApiVersioning features. - Refactor Router to forward apiVersioningConfig from Router's config to Route. - Also resolve feature name in Router to the actual versioning feature. - Add tests for Router and both versioning features.
1 parent c671411 commit 300c279

File tree

8 files changed

+666
-14
lines changed

8 files changed

+666
-14
lines changed

header-api-versioning/index.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"use strict";
2+
3+
module.exports = function (app, config) {
4+
return {
5+
/**
6+
* Find api version from request header
7+
*
8+
* @param {Object} req Epxress request object
9+
* @returns {number} Api version
10+
*/
11+
findApiVersionFromRequest: function(req) {
12+
var pattern = config.pattern;
13+
if(!pattern) {
14+
throw new Error('pattern not defined in config')
15+
}
16+
17+
var header = req.headers[config.header];
18+
19+
if (header) {
20+
var results = header.match(new RegExp(pattern));
21+
22+
if (results) {
23+
return parseInt(results[1]);
24+
}
25+
}
26+
27+
return undefined;
28+
}
29+
}
30+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
var _ = require('lodash')
2+
, expect = require('expect.js')
3+
, HeaderApiVersioning = require('../index')
4+
5+
describe('Header API versioning', function () {
6+
var request;
7+
var config;
8+
var app;
9+
var feature;
10+
11+
beforeEach(function () {
12+
// Mock express app
13+
app = {};
14+
15+
// Mock feature config
16+
config = {
17+
header: 'accept',
18+
pattern: 'application\/vnd.someapplication\.v(\\d+)\\+[A-Za-z]+'
19+
};
20+
21+
// Mock express request object.
22+
request = {
23+
headers: {
24+
'accept': 'application/vnd.someapplication.v1+json'
25+
}
26+
};
27+
28+
feature = HeaderApiVersioning(app, config);
29+
});
30+
31+
it('should return api version correctly', function() {
32+
var apiVersion = feature.findApiVersionFromRequest(request);
33+
expect(apiVersion).equal(1);
34+
});
35+
36+
it('should not return api version with wrong header formatting', function() {
37+
request.headers.accept = 'application/vnd.someapplication+v1.json'
38+
var apiVersion = feature.findApiVersionFromRequest(request);
39+
expect(apiVersion).equal(undefined);
40+
});
41+
42+
it('should throw an exception if pattern is not defined in config', function() {
43+
delete config.pattern;
44+
feature = HeaderApiVersioning(app, config);
45+
46+
expect(function() {
47+
feature.findApiVersionFromRequest(request);
48+
}).throwError();
49+
});
50+
51+
});

route-api-versioning/index.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use strict";
2+
3+
var _ = require('lodash');
4+
5+
module.exports = function (app, config) {
6+
return {
7+
/**
8+
* Find api version from request header
9+
*
10+
* @param {Object} req Epxress request object
11+
* @returns {number} Api version
12+
*/
13+
findApiVersionFromRequest: function(req) {
14+
var apiVersion = req.params.apiVersion;
15+
if (apiVersion) {
16+
var match = apiVersion.match(new RegExp(_.get(config, 'pattern', 'v(\\d+)')));
17+
if (match) {
18+
return parseInt(match[1]);
19+
}
20+
}
21+
return undefined;
22+
},
23+
24+
/**
25+
* Generates path for route
26+
*
27+
* @param {string} path Original route path
28+
* @returns {string} Generated route path
29+
*/
30+
generateRoutePath: function(path) {
31+
return _.get(config, 'routePattern', '/:apiVersion(v\\d+)?') + path;
32+
}
33+
}
34+
35+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
var _ = require('lodash')
2+
, expect = require('expect.js')
3+
, RouteApiVersioning = require('../index')
4+
5+
describe('Route API versioning', function () {
6+
var request;
7+
var config;
8+
var app;
9+
var feature;
10+
11+
describe('default configuration', function() {
12+
beforeEach(function () {
13+
// Mock express app
14+
app = {};
15+
16+
// Mock express request object.
17+
request = {
18+
params: {
19+
apiVersion: 'v1',
20+
other: 'some'
21+
}
22+
};
23+
24+
feature = RouteApiVersioning(app, {});
25+
});
26+
27+
it('should return api version correctly', function() {
28+
var apiVersion = feature.findApiVersionFromRequest(request);
29+
expect(apiVersion).equal(1);
30+
});
31+
32+
it('should not return api version with wrong route formatting', function() {
33+
request.params.apiVersion = 'b5'
34+
var apiVersion = feature.findApiVersionFromRequest(request);
35+
expect(apiVersion).equal(undefined);
36+
});
37+
38+
it('should generate path correctly', function() {
39+
var originalPath = '/some/path';
40+
var path = feature.generateRoutePath(originalPath);
41+
expect(path).equal('/:apiVersion(v\\d+)?' + originalPath);
42+
});
43+
44+
});
45+
46+
describe('defined configuration', function() {
47+
beforeEach(function () {
48+
// Mock express app
49+
app = {};
50+
51+
// Mock feature config
52+
config = {
53+
routePattern: '/:apiVersion(cool\\d+)?',
54+
pattern: 'cool(\\d+)'
55+
};
56+
57+
// Mock express request object.
58+
request = {
59+
params: {
60+
apiVersion: 'cool1',
61+
}
62+
};
63+
64+
feature = RouteApiVersioning(app, config);
65+
});
66+
67+
it('should return api version correctly', function() {
68+
var apiVersion = feature.findApiVersionFromRequest(request);
69+
expect(apiVersion).equal(1);
70+
});
71+
72+
it('should not return api version with wrong route formatting', function() {
73+
request.params.apiVersion = 'v5'
74+
var apiVersion = feature.findApiVersionFromRequest(request);
75+
expect(apiVersion).equal(undefined);
76+
});
77+
78+
it('should generate path correctly', function() {
79+
var originalPath = '/some/path';
80+
var path = feature.generateRoutePath(originalPath);
81+
expect(path).equal(config.routePattern + originalPath);
82+
});
83+
84+
});
85+
86+
});

route/index.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,16 @@ module.exports = function (app, config) {
8888
var rootPath = module.module.rootPath || '/'
8989
, expressRouter
9090
, router = routers[rootPath];
91+
92+
var apiVersioningConfig = _.cloneDeep(config.apiVersioning);
93+
// Replace api versioning feature name with the actual feature instance
94+
if (apiVersioningConfig && apiVersioningConfig.enabled === true) {
95+
apiVersioningConfig.feature = app.feature(apiVersioningConfig.feature);
96+
}
9197

9298
if (!router) {
9399
expressRouter = express.Router();
94-
router = new Router(expressRouter, config.defaultAuthHandler, config.unauthenticatedStatusCode);
100+
router = new Router(expressRouter, config.defaultAuthHandler, config.unauthenticatedStatusCode, apiVersioningConfig);
95101

96102
if (!testing) {
97103
router = addLoggingToRouter(router, rootPath);

0 commit comments

Comments
 (0)