├── .eslintrc.json ├── .gitignore ├── .husky └── pre-commit ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── lib ├── error-codes.js └── helpers.js └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "airbnb-base" 9 | ], 10 | "parserOptions": { 11 | "ecmaVersion": "latest" 12 | }, 13 | "rules": { 14 | "comma-dangle": ["warn", { 15 | "arrays": "always", 16 | "objects": "always", 17 | "imports": "never", 18 | "exports": "never", 19 | "functions": "never" 20 | }], 21 | "semi": ["warn", "never"], 22 | "no-console": "error" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | node_modules/ 4 | npm-debug.log 5 | package-lock.json 6 | .idea 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npm run lint:fix && git add . 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2019 Valerii Kuzivanov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON-RPC node.js implementation 2 | 3 | [JSON-RPC](https://www.jsonrpc.org/specification) official spec. 4 | 5 | Extremely fast and simple Node.js JSON-RPCv2 router middleware. 6 | Handle incoming request and apply to controller functions. 7 | Validation available. 8 | 9 | **Note** As Router require body-parser which must be used before router. 10 | 11 | ## Installation 12 | 13 | ```sh 14 | $ npm install express express-json-rpc-router 15 | ``` 16 | or 17 | 18 | ```sh 19 | $ yarn add express express-json-rpc-router 20 | ``` 21 | 22 | ## Examples 23 | 24 | ### Simple 25 | 26 | 27 | ```js 28 | const express = require('express') 29 | const jsonRouter = require('express-json-rpc-router') 30 | const app = express() 31 | 32 | const controller = { 33 | testMethod({ username }) { 34 | console.log('username: ', username) 35 | return ['example data 1', 'example data 2'] 36 | } 37 | } 38 | 39 | app.use(express.json()) 40 | app.use(jsonRouter({ methods: controller })) 41 | app.listen(3000, () => console.log('Example app listening on port 3000')) 42 | ``` 43 | 44 | and 45 | ```bash 46 | curl -X POST \ 47 | http://localhost:3000 \ 48 | -H 'Content-Type: application/json' \ 49 | -d '{ 50 | "jsonrpc": "2.0", 51 | "method": "testMethod", 52 | "params": { 53 | "username": "valeron" 54 | }, 55 | "id": 1 56 | }' 57 | ``` 58 | will return: 59 | ```json 60 | { 61 | "jsonrpc": "2.0", 62 | "result": [ 63 | "example data 1", 64 | "example data 2" 65 | ], 66 | "id": 1 67 | } 68 | ``` 69 | 70 | ### With Validation, after hooks and onError callback 71 | 72 | 73 | ```js 74 | const controller = { 75 | // You have access to raw express req/res object as raw.res and raw.req 76 | testMethod({ username }, raw) { 77 | console.log('username: ', username) 78 | return ['example data 1', 'example data 2'] 79 | } 80 | } 81 | 82 | const beforeController = { 83 | // You have access to raw express req/res object as raw.res and raw.req 84 | testMethod(params, _, raw) { 85 | if (Math.random() >= 0.5) { // Random error 86 | const error = new Error('Something going wrong') 87 | error.data = { hello: 'world' } // its optional 88 | throw error 89 | } 90 | } 91 | } 92 | 93 | const afterController = { 94 | testMethod: [ 95 | // You have access to result and to raw express req/res object as raw.res and raw.req. 96 | (params, result, raw) => console.log('testMethod executed 1!'), () => console.log('testMethod executed 2!') 97 | ] 98 | } 99 | 100 | app.use(bodyParser.json()) 101 | app.use(jsonRouter({ 102 | methods: controller, 103 | beforeMethods: beforeController, 104 | afterMethods: afterController, 105 | onError(err) { 106 | console.log(err) // send report 107 | } 108 | })) 109 | app.listen(3000, () => console.log('Example app listening on port 3000')) 110 | ``` 111 | 112 | and 113 | ```bash 114 | curl -X POST \ 115 | http://localhost:3000 \ 116 | -H 'Content-Type: application/json' \ 117 | -d '{ 118 | "jsonrpc": "2.0", 119 | "method": "testMethod", 120 | "params": { 121 | "username": "valeron" 122 | }, 123 | "id": 1 124 | }' 125 | ``` 126 | will return: 127 | ```json 128 | { 129 | "jsonrpc": "2.0", 130 | "error": { 131 | "code": -32603, 132 | "message": "Something going wrong", 133 | "data": { "hello": "world" } 134 | }, 135 | "id": 1 136 | } 137 | ``` 138 | 139 | ### Changelog: 140 | - v1.2.0 141 | * Added raw { req, res } native express object to controller and hooks as last argument. 142 | * Passed next arguments: `params, null, raw` to beforeController actions and `params, result, raw` to afterController 143 | * Passed additional second argument `params, raw` to controller actions 144 | - v1.3.0 145 | * Added optional err.data payload to comply with JSON RPC specification: "5.1 Error object". 146 | #### Options 147 | 148 | The `express-json-rpc-router` function takes an optional `options` object that may contain any of the following keys: 149 | 150 | ##### methods `type: Object` 151 | You can pass the object of your methods that will be called when a match is made via JSON-RPC `method` field. 152 | 153 | ##### beforeMethods `type: Object>` 154 | You can provide function or array of functions, which will be called before main method with same name are called. 155 | This is the best place for validation. 156 | beforeMethods names should be the same as methods names. 157 | Request params will be passed as first argument. 158 | 159 | ##### afterMethods `type: Object>` 160 | You can provide function or array of functions, which will be called after main method with same name are called. 161 | This is the best place to write logs. 162 | afterMethods names should be the same as methods names. 163 | Method execution result will be passed as second argument. 164 | Request params will be passed as first argument. 165 | 166 | ##### onError `type: function(err, params)` 167 | callback(err, params) {} 168 | Optionally you can pass onError callback which will be called when json-rpc middleware error occurred. 169 | 170 | ## License 171 | 172 | [MIT](LICENSE) 173 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | interface UserConfig { 2 | methods: object; 3 | beforeMethods: object; 4 | afterMethods: object; 5 | onError: (e) => object; 6 | } 7 | 8 | declare function jsonRpcRouter (userConfig: UserConfig): (req: object, res: object, next: () => void) => void; 9 | 10 | export = jsonRpcRouter; 11 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JSON-RPC express middleware 3 | * Copyright(c) 2019 Valerii Kuzivanov. 4 | * MIT Licensed 5 | */ 6 | const { 7 | validateJsonRpcMethod, 8 | validateJsonRpcVersion, 9 | isNil, 10 | isFunction, 11 | validateConfig, 12 | setConfig, 13 | executeHook, 14 | } = require('./lib/helpers') 15 | const { INTERNAL_ERROR, } = require('./lib/error-codes') 16 | 17 | const VERSION = '2.0' 18 | 19 | /** 20 | * 21 | * @param {object} userConfig Custom user router configuration 22 | * @return {function} Express middleware 23 | */ 24 | module.exports = (userConfig) => { 25 | const config = { 26 | methods: {}, 27 | beforeMethods: {}, 28 | afterMethods: {}, 29 | onError: null, 30 | } 31 | 32 | /** 33 | * JSON RPC request handler 34 | * @param {object} body 35 | * @return {Promise} 36 | */ 37 | async function handleSingleReq(body, raw) { 38 | const { 39 | id, method, jsonrpc, params = {}, 40 | } = body 41 | try { 42 | validateJsonRpcVersion(jsonrpc, VERSION) 43 | 44 | validateJsonRpcMethod(method, config.methods) 45 | 46 | const beforeMethod = config.beforeMethods[method] 47 | if (beforeMethod) { 48 | await executeHook(beforeMethod, params, null, raw) 49 | } 50 | 51 | const result = await config.methods[method](params, raw) 52 | 53 | const afterMethod = config.afterMethods[method] 54 | if (afterMethod) { 55 | await executeHook(afterMethod, params, result, raw) 56 | } 57 | 58 | if (!isNil(id)) return { jsonrpc, result, id, } 59 | } catch (err) { 60 | if (isFunction(config.onError)) config.onError(err, body) 61 | const error = { 62 | code: Number(err.code || err.status || INTERNAL_ERROR.code), 63 | message: err.message || INTERNAL_ERROR.message, 64 | } 65 | if (err && err.data) error.data = err.data 66 | return { jsonrpc, error, id: id || null, } 67 | } 68 | return null 69 | } 70 | 71 | /** 72 | * Batch rpc request handler 73 | * @param {Array} bachBody 74 | * @return {Promise} 75 | */ 76 | function handleBatchReq(bachBody, raw) { 77 | return Promise.all( 78 | bachBody.reduce((memo, body) => { 79 | const result = handleSingleReq(body, raw) 80 | if (!isNil(body.id)) memo.push(result) 81 | return memo 82 | }, []) 83 | ) 84 | } 85 | 86 | validateConfig(userConfig) 87 | 88 | setConfig(config, userConfig) 89 | 90 | return async (req, res, next) => { 91 | const rpcData = req.body 92 | if (Array.isArray(rpcData)) { 93 | res.send(await handleBatchReq(rpcData, { req, res, })) 94 | } else if (typeof rpcData === 'object') { 95 | res.send(await handleSingleReq(rpcData, { req, res, })) 96 | } else { 97 | next(new Error('JSON-RPC router error: req.body is required. Ensure that you install body-parser and apply it before json-router.')) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/error-codes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Default JSON-RPC errors, see https://www.jsonrpc.org/specification#error_object 3 | */ 4 | module.exports = { 5 | PARSE_ERROR: { 6 | code: -32700, 7 | message: 'Parse error', 8 | }, 9 | INVALID_REQUEST: { 10 | code: -32600, 11 | message: 'Invalid Request', 12 | }, 13 | METHOD_NOT_FOUND: { 14 | code: -32601, 15 | message: 'Method not found', 16 | }, 17 | INVALID_PARAMS: { 18 | code: -32602, 19 | message: 'Invalid params', 20 | }, 21 | INTERNAL_ERROR: { 22 | code: -32603, 23 | message: 'Internal error', 24 | }, 25 | SERVER_ERROR: { 26 | code: -32000, 27 | message: 'Server error', 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | const { INVALID_REQUEST, METHOD_NOT_FOUND, } = require('./error-codes') 2 | 3 | /** 4 | * Just throw an error 5 | * @param {string} message 6 | * @param {number} code 7 | */ 8 | exports.throwRpcErr = (message = 'JSON-RPC error', code = 500) => { 9 | const err = new Error(message) 10 | err.code = code 11 | throw err 12 | } 13 | 14 | /** 15 | * Validation for JSON-RPC version 16 | * @param {string} version 17 | * @param {string} requiredVersion 18 | */ 19 | exports.validateJsonRpcVersion = (version, requiredVersion) => { 20 | if (version !== requiredVersion) { 21 | this.throwRpcErr(`${INVALID_REQUEST.message}, wrong version - ${version}`, INVALID_REQUEST.code) 22 | } 23 | } 24 | 25 | /** 26 | * Validation for JSON-RPC method passed from browser 27 | * @param {string} method 28 | * @param {array} controller, list of existing methods 29 | */ 30 | exports.validateJsonRpcMethod = (method, controller) => { 31 | if (!method || typeof method !== 'string') { 32 | this.throwRpcErr(`${INVALID_REQUEST.message}, wrong method - ${method}`, INVALID_REQUEST.code) 33 | } else if (!(method in controller)) { 34 | this.throwRpcErr(`${METHOD_NOT_FOUND.message} - ${method}`, METHOD_NOT_FOUND.code) 35 | } 36 | } 37 | 38 | /** 39 | * Check is value nullable. 40 | * @param {any} val 41 | * @return {boolean} 42 | */ 43 | exports.isNil = (val) => val == null 44 | 45 | /** 46 | * Check is value function. 47 | * @param {any} fn 48 | * @return {boolean} 49 | */ 50 | exports.isFunction = (fn) => typeof fn === 'function' 51 | 52 | /** 53 | * Validate passed user config 54 | * @param config 55 | */ 56 | exports.validateConfig = (config) => { 57 | if (typeof config !== 'object') { 58 | this.throwRpcErr('JSON-RPC error: userConfig should be an object.') 59 | } 60 | if (typeof config.methods !== 'object' || Array.isArray(config.methods)) { 61 | this.throwRpcErr('JSON-RPC error: methods should be an object') 62 | } 63 | if ('beforeMethods' in config) { 64 | if (typeof config.beforeMethods !== 'object' || Array.isArray(config.beforeMethods)) { 65 | this.throwRpcErr('JSON-RPC error: beforeMethods should be an object') 66 | } 67 | 68 | Object.keys(config.beforeMethods).forEach((before) => { 69 | if (!(before in config.methods)) { 70 | this.throwRpcErr(`JSON-RPC error: beforeMethod should have the same name as method, passed: ${before}`) 71 | } 72 | }) 73 | } 74 | if ('afterMethods' in config) { 75 | if (typeof config.afterMethods !== 'object' || Array.isArray(config.afterMethods)) { 76 | this.throwRpcErr('JSON-RPC error: afterMethods should be an object') 77 | } 78 | 79 | Object.keys(config.afterMethods).forEach((after) => { 80 | if (!(after in config.methods)) { 81 | this.throwRpcErr(`JSON-RPC error: afterMethods should have the same name as method, passed: ${after}`) 82 | } 83 | }) 84 | } 85 | if ('onError' in config && typeof config.onError !== 'function') { 86 | this.throwRpcErr('JSON-RPC error: onError should be a function') 87 | } 88 | } 89 | 90 | /** 91 | * Merge custom config with default 92 | * @param {Object} config 93 | * @param {Object} userConfig 94 | */ 95 | exports.setConfig = (config, userConfig) => { 96 | Object.assign(config, userConfig) 97 | } 98 | 99 | /** 100 | * Execute passed user hooks 101 | * @param {function|Array} hook 102 | * @param {Object} params 103 | * @param {any} result - method execution result 104 | * @return {void} 105 | */ 106 | exports.executeHook = (hook, params, result, raw) => { 107 | if (this.isFunction(hook)) { 108 | return hook(params, result, raw) 109 | } 110 | if (Array.isArray(hook)) { 111 | return Promise.all( 112 | hook.map((h) => h(params, result, raw)) 113 | ) 114 | } 115 | return this.throwRpcErr('JSON-RPC error: wrong hook type passed') 116 | } 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-json-rpc-router", 3 | "description": "Json-rpc middleware router for express", 4 | "version": "1.4.0", 5 | "author": "Valerii Kuzivanov ", 6 | "license": "MIT", 7 | "repository": "express-json-rpc-router", 8 | "main": "index.js", 9 | "scripts": { 10 | "lint": "eslint ./**/*.js ./*.js", 11 | "lint:fix": "eslint ./**/*.js ./*.js --fix", 12 | "prepare": "husky install" 13 | }, 14 | "devDependencies": { 15 | "eslint": "^8.15.0", 16 | "eslint-config-airbnb-base": "^15.0.0", 17 | "eslint-plugin-import": "^2.26.0", 18 | "husky": "^8.0.1" 19 | }, 20 | "engines": { 21 | "node": ">= 8.0" 22 | }, 23 | "files": [ 24 | "index.js", 25 | "lib/", 26 | "HISTORY.md", 27 | "LICENSE" 28 | ], 29 | "keywords": [ 30 | "json-rpc", 31 | "router", 32 | "middleware", 33 | "jsonrpc2", 34 | "express" 35 | ], 36 | "bugs": { 37 | "url": "https://github.com/Valeronlol/express-json-rpc-router/issues" 38 | }, 39 | "homepage": "https://github.com/Valeronlol/express-json-rpc-router#readme" 40 | } 41 | --------------------------------------------------------------------------------