Express middleware to map requests to functions of components
Within the terminology of HTTP we do have requests and responses. Given a response is just a function of a request, this package provide the option to configure such functional mappings.
In order to structure such functions the words "component" and "composition" are
used in this way:
A component is a set of functions.
A composition is a set of components.
A requested domain (on a specific port) is mapped to a composition.
The purpose of this package is to support such a structure by configuration.
Prerequisites
- nodejs
- expressjs
Auto invalidation of require-cache in development mode
Since nodejs caches required (imported) packages, changes within a file does
not affect a running express server, it needs to be restared. This is expensive
for continuously change-save-and-review cycles web-developers love.
This package implements automatic invalidation of require-cache per
component-package level. If a script file changes, all modules of corresponding
component-package gets invalidated.
Example composition
Let's assume we compose a todo-list component together with a calendar component. Composition's package directory consists of:
config.js
package.json
server.js
config.js (map request to components based on context-path)
//composition config
module.exports = {
//a composition is a set of components ...
components:{
'@yourOrg/todo-list':{
public: true,
context_path: '/todo'
},
'@yourOrg/calendar':{
public: true,
context_path: '/calendar'
}
}
}
npm's package.json
{
"name": "@yourScope/example-composition",
"version": "1.0.0",
"main": "server.js",
"dependencies": {
"express": "^4.17.1",
"@pubcore/node-composition": "^2.8.0",
"@yourScope/todo-list": "^0.1.0",
"@yourScope/calendar": "^0.1.0",
}
}
server.js
const
express = require('express'),
app = express(),
composition = require('@pubcore/node-composition').default,
config = require('./config.js')
app.use('/', composition(config, require))
Configuration options
module.exports = {
componentDefault:{
//if true, login (next option) is required
public: false
//login middleware, required, if component is not public
login: (req, res, next) => {next()},
//optional, build arbitrary data added to req.resources
resources: async (req) => {}
//optional error handler middleware
error: (err, req, res, next) => {},
//optional urlencoder middleware
//see http://expressjs.com/de/api.html#express.urlencoded
urlencoded: {extended: true}
},
components: {
"@company/component-one":{
//component ID
id: "@company/component-one"
//context path used for express Router
context_path: "/basePathOfComponentOne"
//optional to define (overwrite) defaults, see "componentDefault" ...
}
},
accesscontrol:{
//see https://developer.mozilla.org/en-US/docs/Glossary/CORS
//CORS headers are responded for requests send from sites of following
allowedOrigins: ["https://foo.net"],
//see https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
contentSecurityPolicy: "default-src 'self' data:; script-src 'self' font-src https:; style-src 'unsafe-inline' https:;"
//Cross Site Request Forgery (SCRF) Protection by Double Submit Cookie Pattern
//see https://github.com/expressjs/csurf
//Secure config to store token-secret by cookie:
csrfProtection: {key:'__Host-Csrf', secure:true, sameSite:'lax', httpOnly:true}
},
//optional
options:{
//optional
requestJsonLimit: '2mb' //default 100kb
}
}
Example component
A component package exports the mapping of URI sub-path to a express middleware function:
- src/index.js
//import express middleware functions
import list from './lib/getList'
import addItem from './lib/addItem'
export default {
public:true,
http: [
{
routePath: '/list',
map: list,
method: 'GET',
accepted: ['text/plain']
},
{
routePath: '/list',
map: addItem,
method: 'POST',
accepted: ['application/json'],
urlencoded: {extended: true} //optional, see above
},
]
}
- Optional, public "htdocs" directory contain some static files (e.g. imgage, css, js)
htdocs/
Features test output
compose components by configuration
✓ serves requests for some configured component functions
✓ serves requests for configured second "component-two"
✓ requires a login middleware function, if component is private
✓ reloads modules in development mode, if corresponding js file changed (131ms)
✓ reloads modules in development mode, if corresponding js file changed (113ms)
✓ supports CORS - CrossOriginResourceSharing by config (allowedOrigins)
✓ sends CSP - Content Security Policy header, if configured
✓ offers req.cookies and req.cookiesByArray object, if there are cookies
compose, if validation of a component fails
✓ should skip corresponding component and response with status 500
compose, if environment is in PRODUCTION mode
✓ does not load changed module’s script file and shows same result as before
component router
✓ routes requests based on component config
✓ support different methods for same path
✓ checks accept header
✓ checks http method
✓ checks http method before login
dev-mode: synchronous reload for @scope-a/component-one
dev-mode: synchronous reload for ./js/index
✓ responses "not found" for other paths
✓ requires authentication, if component or function is not public
✓ invokes a "login" promise for private resources
✓ removes passwort after login, for security reasons
✓ invokes a "resources" promise, if configured
✓ invokes a "resources" promise, if configured and use config data
✓ it catches up failed "resources" promise
✓ supports error handler middleware
✓ supports Content-Type: application/x-www-form-urlencoded
✓ supports to turn off "urlencoded" middleware endpoint specific
Cross Site Request Forgery protection
✓ response a session-secret cookie, based on config)
✓ serves 403, if token is invalid
✓ accepts valid token send by form hidden field "_csrf"