├── .eslintignore ├── .github ├── express-version-route.png └── workflows │ └── main.yml ├── .gitignore ├── errors └── index.js ├── LICENSE ├── .editorconfig ├── index.js ├── package.json ├── test └── sanity.test.js └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | -------------------------------------------------------------------------------- /.github/express-version-route.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/localnerve/express-version-route/master/.github/express-version-route.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dccache 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | node_modules 8 | 9 | # Coverage 10 | lib-cov 11 | coverage 12 | .nyc_output 13 | 14 | # IDE 15 | *.iml 16 | .idea 17 | 18 | # Docs 19 | docs/ 20 | -------------------------------------------------------------------------------- /errors/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | class RouteVersionUnmatchedError extends Error { 3 | get name () { 4 | return this.constructor.name 5 | } 6 | } 7 | 8 | module.exports = { 9 | RouteVersionUnmatchedError 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Liran Tal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | # Howto with your editor: http://editorconfig.org/#download 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file 8 | [**] 9 | end_of_line = lf 10 | insert_final_newline = true 11 | charset = utf-8 12 | 13 | # Standard at: https://github.com/felixge/node-style-guide 14 | [**.{js,json}] 15 | trim_trailing_whitespace = true 16 | indent_style = space 17 | indent_size = 2 18 | quote_type = single 19 | 20 | # LT: the following are enforced by standardjs so I commented 21 | #curly_bracket_next_line = false 22 | #spaces_around_operators = true 23 | #space_after_control_statements = true 24 | #space_after_anonymous_functions = true 25 | #spaces_in_brackets = false 26 | 27 | # No Standard. Please document a standard if different from .js 28 | [**.{yml,css}] 29 | trim_trailing_whitespace = true 30 | indent_style = space 31 | 32 | [**.html] 33 | trim_trailing_whitespace = true 34 | indent_style = space 35 | indent_size = 2 36 | 37 | # No standard. Please document a standard if different from .js 38 | [**.md] 39 | indent_style = tab 40 | 41 | # Standard at: 42 | [Makefile] 43 | indent_style = tab 44 | 45 | # The indentation in package.json will always need to be 2 spaces 46 | # https://github.com/npm/npm/issues/4718 47 | [{package, bower}.json] 48 | indent_style = space 49 | indent_size = 2 50 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | platform: [ubuntu-latest] 10 | node: ['12', '10'] 11 | name: Node ${{ matrix.node }} (${{ matrix.platform }}) 12 | runs-on: ${{ matrix.platform }} 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: ${{ matrix.node }} 18 | - name: install dependencies 19 | run: yarn install --ignore-engines 20 | - name: install codecov 21 | run: npm install -g codecov 22 | - name: run tests 23 | run: yarn run test:coverage 24 | - name: get code coverage report 25 | run: yarn run report-coverage 26 | - name: send coverage 27 | run: codecov 28 | env: 29 | CODECOV_TOKEN: ${{secrets.CODECOV_TOKEN}} 30 | 31 | release: 32 | name: do semantic release 33 | runs-on: 'ubuntu-latest' 34 | needs: build 35 | steps: 36 | - uses: actions/checkout@v1 37 | - uses: actions/setup-node@v1 38 | with: 39 | node-version: '10' 40 | - name: install dependencies 41 | run: yarn install --ignore-engines 42 | - name: release 43 | run: yarn run semantic-release 44 | env: 45 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 46 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 47 | if: github.event_name == 'push' && github.ref == 'refs/heads/master' 48 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const semver = require('semver') 4 | const { RouteVersionUnmatchedError } = require('./errors') 5 | 6 | class versionRouter { 7 | static route (versionsMap = new Map(), options = new Map()) { 8 | return (req, res, next) => { 9 | var versionArray = [] 10 | for (let [versionKey, versionRouter] of versionsMap) { 11 | versionArray.push(versionKey) 12 | if (this.checkVersionMatch(req.version, versionKey)) { 13 | return versionRouter(req, res, next) 14 | } 15 | } 16 | 17 | if (options.useMaxVersion) { 18 | const maxVersion = semver.maxSatisfying(versionArray, req.version) 19 | if (maxVersion) { 20 | for (let [versionKey, versionRouter] of versionsMap) { 21 | if (this.checkVersionMatch(maxVersion, versionKey)) { 22 | return versionRouter(req, res, next) 23 | } 24 | } 25 | } 26 | } 27 | 28 | const defaultRoute = this.getDefaultRoute(versionsMap) 29 | if (defaultRoute) { 30 | return defaultRoute(req, res, next) 31 | } 32 | 33 | return next(new RouteVersionUnmatchedError(`${req.version} doesn't match any versions`)) 34 | } 35 | } 36 | 37 | static checkVersionMatch (requestedVersion, routeVersion) { 38 | return semver.valid(requestedVersion) && semver.satisfies(requestedVersion, routeVersion) 39 | } 40 | 41 | static getDefaultRoute (options = new Map()) { 42 | return options.get('default') 43 | } 44 | } 45 | 46 | module.exports = versionRouter 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-version-route", 3 | "version": "0.0.0-development", 4 | "description": "provides middleware to load controllers based on api versions", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "standard && eslint *.js", 8 | "lint:fix": "standard --fix", 9 | "test": "yarn run lint && ava", 10 | "test:watch": "yarn run lint && ava --watch", 11 | "test:coverage": "yarn run lint && nyc --reporter=lcov ava --tap", 12 | "coverage:view": "opn coverage/lcov-report/index.html", 13 | "report-coverage": "nyc report --reporter=text-lcov > coverage.lcov", 14 | "commit": "git-cz", 15 | "docs": "yarn run docs:code && yarn run docs:api", 16 | "docs:api": "doxdox *.js --layout bootstrap --output docs/index.html", 17 | "docs:code": "docco *.js --output docs/code", 18 | "semantic-release": "semantic-release" 19 | }, 20 | "author": "Liran Tal", 21 | "license": "MIT", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/lirantal/express-version-route.git" 25 | }, 26 | "homepage": "https://github.com/lirantal/express-version-route#readme", 27 | "devDependencies": { 28 | "@commitlint/cli": "^7.0.0", 29 | "@commitlint/config-angular": "^7.0.1", 30 | "ava": "^2.0.0", 31 | "commitizen": "^3.0.0", 32 | "cz-conventional-changelog": "^2.1.0", 33 | "docco": "^0.7.0", 34 | "doxdox": "^2.0.1", 35 | "eslint": "^6.0.0", 36 | "eslint-plugin-ava": "^5.1.0", 37 | "eslint-plugin-import": "^2.2.0", 38 | "eslint-plugin-node": "^5.2.1", 39 | "eslint-plugin-security": "^1.3.0", 40 | "husky": "^3.0.0", 41 | "nyc": "^13.2.0", 42 | "opn-cli": "^4.0.0", 43 | "semantic-release": "^15.8.1", 44 | "standard": "^12.0.0" 45 | }, 46 | "nyc": { 47 | "statements": 85, 48 | "branches": 85, 49 | "functions": 85, 50 | "lines": 85, 51 | "reporter": [ 52 | "lcov", 53 | "text-summary" 54 | ], 55 | "cache": true, 56 | "check-coverage": true 57 | }, 58 | "config": { 59 | "commitizen": { 60 | "path": "./node_modules/cz-conventional-changelog" 61 | } 62 | }, 63 | "husky": { 64 | "hooks": { 65 | "commit-msg": "commitlint --env HUSKY_GIT_PARAMS", 66 | "pre-commit": "yarn run lint", 67 | "pre-push": "yarn run lint && yarn run test", 68 | "post-merge": "yarn install", 69 | "post-checkout": "git status", 70 | "post-commit": "git status" 71 | } 72 | }, 73 | "commitlint": { 74 | "extends": [ 75 | "@commitlint/config-angular" 76 | ] 77 | }, 78 | "eslintConfig": { 79 | "env": { 80 | "node": true, 81 | "es6": true 82 | }, 83 | "plugins": [ 84 | "node", 85 | "security", 86 | "ava" 87 | ], 88 | "extends": [ 89 | "plugin:ava/recommended", 90 | "plugin:node/recommended" 91 | ], 92 | "rules": { 93 | "node/no-unsupported-features": "off", 94 | "node/no-unpublished-require": "off", 95 | "security/detect-non-literal-fs-filename": "error", 96 | "security/detect-unsafe-regex": "error", 97 | "security/detect-buffer-noassert": "error", 98 | "security/detect-child-process": "error", 99 | "security/detect-disable-mustache-escape": "error", 100 | "security/detect-eval-with-expression": "error", 101 | "security/detect-no-csrf-before-method-override": "error", 102 | "security/detect-non-literal-regexp": "error", 103 | "security/detect-non-literal-require": "error", 104 | "security/detect-object-injection": "error", 105 | "security/detect-possible-timing-attacks": "error", 106 | "security/detect-pseudoRandomBytes": "error" 107 | }, 108 | "parserOptions": { 109 | "ecmaFeatures": { 110 | "impliedStrict": true 111 | } 112 | } 113 | }, 114 | "release": { 115 | "branch": "master", 116 | "analyzeCommits": { 117 | "preset": "angular", 118 | "releaseRules": [ 119 | { 120 | "type": "docs", 121 | "release": "patch" 122 | }, 123 | { 124 | "type": "refactor", 125 | "release": "patch" 126 | }, 127 | { 128 | "type": "style", 129 | "release": "patch" 130 | } 131 | ] 132 | } 133 | }, 134 | "dependencies": { 135 | "semver": "^6.3.0" 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /test/sanity.test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const test = require('ava') 4 | const versionRouter = require('../index') 5 | const { RouteVersionUnmatchedError } = require('../errors') 6 | 7 | test('given a versioned router, match the route', t => { 8 | const v1 = '1.0' 9 | const requestedVersion = '1.0.0' 10 | 11 | const routesMap = new Map() 12 | routesMap.set(v1, (req, res, next) => { 13 | res.out = { testVersion: v1 } 14 | return res.out 15 | }) 16 | 17 | const middleware = versionRouter.route(routesMap) 18 | const req = { 19 | version: requestedVersion 20 | } 21 | 22 | const result = middleware(req, {}, () => { }) 23 | t.is(result.testVersion, v1) 24 | }) 25 | 26 | test('given a versioned router, dont match if requestVersion is not semver syntax', t => { 27 | const v1 = '1.0' 28 | const requestedVersion = '1.0' 29 | 30 | const routesMap = new Map() 31 | routesMap.set(v1, (req, res, next) => { 32 | res.out = { testVersion: v1 } 33 | return res.out 34 | }) 35 | 36 | const middleware = versionRouter.route(routesMap) 37 | const req = { 38 | version: requestedVersion 39 | } 40 | 41 | const result = middleware(req, {}, () => { }) 42 | t.falsy(result) 43 | }) 44 | 45 | test('given a versioned router, dont match the requestVersion and error out if no match exists', t => { 46 | const v1 = '1.0' 47 | const requestedVersion = '3.0.0' 48 | 49 | const routesMap = new Map() 50 | routesMap.set(v1, (req, res, next) => { 51 | res.out = { testVersion: v1 } 52 | return res.out 53 | }) 54 | 55 | const nextHandler = (err) => { 56 | return err 57 | } 58 | 59 | const middleware = versionRouter.route(routesMap) 60 | const req = { 61 | version: requestedVersion 62 | } 63 | 64 | let resIn = null 65 | const result = middleware(req, resIn, nextHandler) 66 | t.falsy(resIn) 67 | t.truthy(result instanceof Error) 68 | t.truthy(result instanceof RouteVersionUnmatchedError) 69 | t.true(result.name === 'RouteVersionUnmatchedError') 70 | t.true(result.message === `${requestedVersion} doesn't match any versions`) 71 | }) 72 | 73 | test('given 2 versions, first version matches', t => { 74 | const v1 = '1.0' 75 | const v2 = '2.0' 76 | const requestedVersion = '1.0.0' 77 | 78 | const routesMap = new Map() 79 | routesMap.set(v1, (req, res, next) => { 80 | res.out = { testVersion: v1 } 81 | return res.out 82 | }) 83 | 84 | routesMap.set(v2, (req, res, next) => { 85 | res.out = { testVersion: v2 } 86 | return res.out 87 | }) 88 | 89 | const middleware = versionRouter.route(routesMap) 90 | const req = { 91 | version: requestedVersion 92 | } 93 | 94 | const result = middleware(req, {}, () => { }) 95 | t.is(result.testVersion, v1) 96 | }) 97 | 98 | test('given 2 overlapping matching versions, first match wins', t => { 99 | const v1 = '>=1.0' 100 | const v2 = '>=2.0' 101 | const requestedVersion = '2.0.0' 102 | 103 | const routesMap = new Map() 104 | routesMap.set(v2, (req, res, next) => { 105 | res.out = { testVersion: v2 } 106 | return res.out 107 | }) 108 | 109 | routesMap.set(v1, (req, res, next) => { 110 | res.out = { testVersion: v1 } 111 | return res.out 112 | }) 113 | 114 | const middleware = versionRouter.route(routesMap) 115 | const req = { 116 | version: requestedVersion 117 | } 118 | 119 | const result = middleware(req, {}, () => { }) 120 | t.is(result.testVersion, v2) 121 | }) 122 | 123 | test('given 2 versions, second version matches so the map insertion order doesnt count', t => { 124 | const v1 = '1.0' 125 | const v2 = '2.0' 126 | const requestedVersion = '1.0.0' 127 | 128 | const routesMap = new Map() 129 | routesMap.set(v2, (req, res, next) => { 130 | res.out = { testVersion: v2 } 131 | return res.out 132 | }) 133 | 134 | routesMap.set(v1, (req, res, next) => { 135 | res.out = { testVersion: v1 } 136 | return res.out 137 | }) 138 | 139 | const middleware = versionRouter.route(routesMap) 140 | const req = { 141 | version: requestedVersion 142 | } 143 | 144 | const result = middleware(req, {}, () => { }) 145 | t.is(result.testVersion, v1) 146 | }) 147 | 148 | test('when no version mapping is provided, dont match any route', t => { 149 | const requestedVersion = '1.0.0' 150 | 151 | const middleware = versionRouter.route() 152 | const req = { 153 | version: requestedVersion 154 | } 155 | 156 | const result = middleware(req, {}, () => { }) 157 | t.falsy(result) 158 | }) 159 | 160 | test('given 2 versions and a default, if no match is found the default route should be used', t => { 161 | const v1 = '1.0' 162 | const v2 = '2.0' 163 | const requestedVersion = '3.0.0' 164 | 165 | const routesMap = new Map() 166 | routesMap.set(v2, (req, res, next) => { 167 | res.out = { testVersion: v2 } 168 | return res.out 169 | }) 170 | 171 | routesMap.set(v1, (req, res, next) => { 172 | res.out = { testVersion: v1 } 173 | return res.out 174 | }) 175 | 176 | routesMap.set('default', (req, res, next) => { 177 | res.out = { testVersion: 'default' } 178 | return res.out 179 | }) 180 | 181 | const middleware = versionRouter.route(routesMap) 182 | const req = { 183 | version: requestedVersion 184 | } 185 | 186 | const result = middleware(req, {}, () => { }) 187 | t.is(result.testVersion, 'default') 188 | }) 189 | 190 | test('given 2 versions, max version matches', t => { 191 | const v1 = '1.2.0' 192 | const v2 = '1.2.3' 193 | const requestedVersion = '1.2' 194 | 195 | const routesMap = new Map() 196 | routesMap.set(v1, (req, res, next) => { 197 | res.out = { testVersion: v1 } 198 | return res.out 199 | }) 200 | 201 | routesMap.set(v2, (req, res, next) => { 202 | res.out = { testVersion: v2 } 203 | return res.out 204 | }) 205 | 206 | const middleware = versionRouter.route(routesMap, { useMaxVersion: true }) 207 | const req = { 208 | version: requestedVersion 209 | } 210 | 211 | const result = middleware(req, {}, () => { }) 212 | t.is(result.testVersion, v2) 213 | }) 214 | 215 | test('given 2 versions, if no max version default matches', t => { 216 | const v1 = '1.2.0' 217 | const v2 = '1.2.3' 218 | const v3 = 'default' 219 | const requestedVersion = '1.2' 220 | 221 | const routesMap = new Map() 222 | routesMap.set(v1, (req, res, next) => { 223 | res.out = { testVersion: v1 } 224 | return res.out 225 | }) 226 | 227 | routesMap.set(v2, (req, res, next) => { 228 | res.out = { testVersion: v2 } 229 | return res.out 230 | }) 231 | 232 | routesMap.set(v3, (req, res, next) => { 233 | res.out = { testVersion: v3 } 234 | return res.out 235 | }) 236 | 237 | const middleware = versionRouter.route(routesMap) 238 | const req = { 239 | version: requestedVersion 240 | } 241 | 242 | const result = middleware(req, {}, () => { }) 243 | t.is(result.testVersion, v3) 244 | }) 245 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # express-version-route 2 | 3 | [![view on npm](http://img.shields.io/npm/v/express-version-route.svg)](https://www.npmjs.org/package/express-version-route) 4 | [![view on npm](http://img.shields.io/npm/l/express-version-route.svg)](https://www.npmjs.org/package/express-version-route) 5 | [![Build](https://github.com/lirantal/express-version-route/workflows/CI/badge.svg?branch=master&event=push)](https://github.com/lirantal/express-version-route/actions?query=workflow%3ACI) 6 | [![Codecov](https://img.shields.io/codecov/c/gh/lirantal/express-version-route.svg)](https://codecov.io/gh/lirantal/express-version-route) 7 | [![npm module downloads](http://img.shields.io/npm/dt/express-version-route.svg)](https://www.npmjs.org/package/express-version-route) 8 | [![Known Vulnerabilities](https://snyk.io/test/github/lirantal/express-version-route/badge.svg)](https://snyk.io/test/github/lirantal/express-version-route) 9 | [![Security Responsible Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow.svg)](https://github.com/nodejs/security-wg/blob/master/processes/responsible_disclosure_template.md 10 | ) 11 | 12 | [![express-version-route](https://snyk.io/advisor/npm-package/express-version-route/badge.svg)](https://snyk.io/advisor/npm-package/express-version-route) 13 | 14 | This npm package provides an Express middleware to load route controllers based on api versions. 15 | 16 | Implementing API Versioning as simple as: 17 | 18 | ![](.github/express-version-route.png) 19 | 20 | now any request would be handled with the appropriate route handler in accordance to `request.version`. 21 | 22 | ## Usage 23 | 24 | Create a map where the key is the version of the supported controller, and the value is a regular Express route function signature. 25 | 26 | ```js 27 | const versionRouter = require('express-version-route') 28 | 29 | const routesMap = new Map() 30 | routesMap.set('1.0', (req, res, next) => { 31 | return res.status(200).json({'message': 'hello to you version 1.0'}) 32 | }) 33 | ``` 34 | 35 | Then, on the route which you wish to version, call the `route` function of this module with the map you created: 36 | 37 | ```js 38 | router.get('/test', versionRouter.route(routesMap)) 39 | ``` 40 | 41 | If no route matches the version requested by a client then the next middleware in the chain will be called. 42 | To set a route fallback incase no version matches set a 'default' key on the routes map, for example: 43 | 44 | ```js 45 | routesMap.set('default', (req, res, next) => { 46 | return res.status(200).json({'message': 'hello to you, this is the default route'}) 47 | }) 48 | ``` 49 | 50 | If maximal possible version (for example to get the latest bugfix) is necessary, then please specify `useMaxVersion: true` in `route` function, then the maximal possible version will be returned for your request. For example for `1.0` request, the version `1.0.2` will be returned: 51 | 52 | ```js 53 | const routesMap = new Map() 54 | routesMap.set('1.0.0', (req, res, next) => { 55 | return res.status(200).json({'message': 'hello to you version 1.0.0'}) 56 | }) 57 | routesMap.set('1.0.2', (req, res, next) => { 58 | return res.status(200).json({'message': 'hello to you version 1.0.2'}) 59 | }) 60 | 61 | router.get('/test', versionRouter.route(routesMap,{useMaxVersion: true})) 62 | ``` 63 | 64 | 65 | ## Usage with TypeScript 66 | 67 | ```ts 68 | import * as versionRouter from 'express-version-route' 69 | import { Router, Handler } from 'express'; 70 | 71 | const router = Router(); 72 | const routesMap = new Map(); 73 | 74 | routesMap.set('1.0', (req, res, next) => { 75 | return res.status(200).json({'message': 'hello to you version 1.0'}) 76 | }) 77 | 78 | routesMap.set('default', (req, res, next) => { 79 | return res.status(200).json({'message': 'hello to you, this is the default route'}) 80 | }) 81 | 82 | router.get('/test', versionRouter.route(routesMap)) 83 | ``` 84 | 85 | ## How it works 86 | 87 | ### The Library 88 | 89 | A requested version from the client must be available on the request object at `req.version`. 90 | You are encouraged to use this module's twin: [express-version-request](https://github.com/lirantal/express-version-request) which is another simple Express middleware that populates `req.version` from the client's X-Api-Version header, Accept header or from a query string (such as 'api-version=1.0.0') 91 | 92 | The key for the routes versions you define can be a non-semver format, for example: `1.0` or just `1`. Under the hood, `expression-version-route` uses the `semver` module to check if the version found on the request object at `req.version` matches the route. 93 | 94 | ### Client-Server flow 95 | 96 | 1. An API client will send a request to your API endpoint with an HTTP header that specifies the requested version of the API to use: 97 | ```bash 98 | curl --header "X-Api-Version: 1.0.0" https://www.example.com/api/users 99 | ``` 100 | 101 | 2. The `express-version-request` library will parse the `X-Api-Version` and sets Express's `req.version` property to 1.0.0. 102 | 3. The `express-version-route` library, when implemented like the usage example above will match the 1.0 route version because semver will match 1.0.0 to 1.0, and then reply with the JSON payload `{'message': 'hello to you version 1.0'}`. 103 | 104 | 105 | ## Installation 106 | 107 | ```bash 108 | yarn add express-version-route 109 | ``` 110 | 111 | ## TypeScript Support 112 | 113 | ```bash 114 | yarn add --dev @types/express-version-route 115 | ``` 116 | 117 | _Note: Don't forget to add types for Express as well!_ 118 | 119 | ## Tests 120 | 121 | ```bash 122 | yarn test 123 | ``` 124 | 125 | Project linting: 126 | 127 | ```bash 128 | yarn lint 129 | ``` 130 | 131 | ## Coverage 132 | 133 | ```bash 134 | yarn test:coverage 135 | ``` 136 | 137 | ## Commit 138 | 139 | The project uses the commitizen tool for standardizing changelog style commit 140 | messages so you should follow it as so: 141 | 142 | ```bash 143 | git add . # add files to staging 144 | yarn commit # use the wizard for the commit message 145 | ``` 146 | 147 | ## On API Versioning... 148 | 149 | An API versioning is a practice that enables services to evolve their APIs with new changes, signatures and the overall API contract without interrupting API consumers and forcing them to repeatedly make changes in order to keep in pace with changes to APIs. 150 | 151 | Several methodologies exist to version your API: 152 | * URL: A request specifies the version for the resource: `http://api.domain.com/api/v1/schools/3/students` 153 | * Query String: A request specifies the resource in a query string: `http://api.domain.com/api/schools/3/students?api-version=1` 154 | * Custom HTTP Header: A request to a resource `http://api.domain.com/api/schools/3/students` with a custom HTTP header set in the request `X-Api-Version: 1` 155 | * MIME Type content negotiation: A request to a resource `http://api.domain.com/api/schools/3/students` with an `Accept` header that specifies the requested content and its version: `Accept: application/vnd.ecma.app-v2+json` 156 | 157 | There is no strict rule on which methodology to follow and each has their own pros and cons. The RESTful approach is the semantic mime-type content negotiation, but a more pragmatic solution is the URL or custom HTTP header. 158 | 159 | ### Why API Versioning at all ? 160 | 161 | Upgrading APIs with some breaking change would lead to breaking existing products, services or even your own frontend web application which is dependent on your API contract. By implementing API versioning you can ensure that changes you make to your underlying API endpoints are not affecting systems that consume them, and using a new version of an API is an opt-in on the consumer. [read more...](https://apigee.com/about/blog/technology/restful-api-design-tips-versioning) 162 | 163 | ## Alternative Node.js libraries 164 | 165 | Several npm projects exist which provide similar API versioning capabilities to Express projects, and I have even contributed Pull Requests to some of them that provide fixes or extra functionality but unfortunately they all seem to be unmaintained, or buggy. 166 | 167 | * https://github.com/Prasanna-sr/express-routes-versioning 168 | * https://github.com/elliotttf/express-versioned-routes 169 | * https://github.com/biowink/express-route-versioning 170 | 171 | ## Author 172 | 173 | Liran Tal 174 | --------------------------------------------------------------------------------