Middleware
You can read more about middlewares on https://expressjs.com/en/guide/using-middleware.html
In general it’s a function that accepts request, response and next callback. This function can analyze requests and add more details to it (like parse JSON, GET query params or check user token). Middleware can pass requests to the next level (next middleware or handler) or can respond directly and finish requests.
This is really powerful and allow developer to reuse simple logic and build route on this simple building blocks
Default
static get middleware() {
return new Map([['/{*splat}', [GetUserByToken, Auth]]]);
}
Middleware order
Middleware will be executed in the provided order. Based on that you can chain middleware where input on second middleware depends on output first middleware
Global middlewares
Framework uses internally few middlewares. This middlewares not adjustable (for now) and executed on each request
Including middlewares into controllers
Controllers level middleware adjusted based on “middleware” getter
static get middleware() {
return new Map([['METHOD/path', ["Middleware","Array"]]]);
// return middlewares for THIS route only
}
Where 'METHOD/path' is a method with a path. Any supported methods by express server supported here (GET, POST, PUT, DELETE, etc). Possible to start router with any method that supported by Express and middleware will be scoped by this method. If middleware route started from "/" then ALL method will be used
Path is part of the express regex path. https://expressjs.com/en/5x/api.html#router.methods
Middleware array - array of middlewares (with params)
Sample
static get middleware() {
return new Map([['GET/{*splat}', [GetUserByToken]]]);
}
static get middleware() {
return new Map([
['POST/someUrl', [
GetUserByToken,
[RoleMiddleware, { roles: ['admin'] }]
]]
]);
}
Middleware here not a raw express middlewares. Please see below
Including middlewares into route object
Middlewares can also be added into route object (subchapter “Routes”)
Middleware parameters
Some middleware acept initial parameters pass into it,
static get middleware() {
return new Map([
['POST/someUrl', [
GetUserByToken // middleware with no parameters
[RoleMiddleware, { roles: ['admin'] }] // middleware with parameters
]]
]);
}
To pass parameters wrap middleware into an array. First element will be middleware inself, second one - middleware parameters. Second on will be pass as it into middleware constructor
Build in middleware
Frameworks have few middlewares that you can use
Auth
import Auth from "@adaptivestone/framework/services/http/middleware/Auth.js";
Allow to pass only if the user provided. Please use any middleware that provide user instance before (like GetUserByToken)
Parameters
No parameters
Cors
import Cors from "@adaptivestone/framework/services/http/middleware/Cors.js";
Add cors headers if origin match config.
Parameters
origins - array of strings of regexp to check original. Required parameter
static get middleware() {
return new Map([
['GET/someUrl', [
[Cors, { origins: ['http://localhost',/./] }]
]]
]);
}
GetUserByToken
import GetUserByToken from "@adaptivestone/framework/services/http/middleware/GetUserByToken.js";
Grab a token and try to parse the user from it. Will find user on databased by token. If user exist will add req.appInfo.user variable.
Parameters
No parameters
I18n
import I18n from "@adaptivestone/framework/services/http/middleware/I18n.js";
Internationalization module based on i18next. Provides req.appInfo.i18n that can be used to translate
Middleware provides few detectors
- X-Lang header
- Query
- User
Please check i18n documentation for more details
Parameters
No parameters
IpDetector
import IpDetector from "@adaptivestone/framework/services/http/middleware/IpDetecor.js";
This middleware will detect client IP. In works well with different proxy (AWS ELB, Nginx, etc) and detect real client ip.
If request ip from trustedProxy (trusted sources) only then module will try to parse IP from provided headers "X-Forwarded-For" and grab client ip from there. Otherwise it will be used request ip
This is a code middleware and some other middlewares (like RateLimiter) depends on it
Parameters
All paramenter goes into config file. There are two parameneter there: headers and trustedProxy
Headers is array of headers to parse ip address. By default it 'X-Forwarded-For'
trustedProxy is a ip, CIDR or range of ipv4 and ipv6 that trusted to parse headers
headers: ['X-Forwarded-For'],
trustedProxy: [ // list of trusted proxies.
'169.254.0.0/16', // ipv4 cidr
'fe80::/10', // ipv6 cidr
'127.0.0.1', // ip itself
'1.1.1.1-1.1.1.3', // ip range
],
Ip data available on
req.appInfo.ip;
Select trustedProxy really carefull as anyone can add any headers to you request. B
Nginx sample to add header
server {
location xxxx/ {
proxy_set_header X-Forwarded-For $remote_addr;
}
}
Pagination
import Pagination from "@adaptivestone/framework/services/http/middleware/Pagination.js";
Pagination middleware provides helper that grabs URL search parameters (page, limit) and calculate necessary appInfo properties (skip, limit and page)
Parameters
limit = 10 - default limit if not provided
maxLimit = 100 max limit for documents
static get middleware() {
return new Map([
['POST/someUrl', [
[Pagination, { limit: 10,maxLimit: 50}]
]]
]);
}
static get middleware() {
return new Map([
['POST/someUrl', [Pagination]
]);
}
// http://localhost:3300/someUrl?limit=10&page=2
const { limit, skip, page } = req.appInfo.pagination;
PrepareAppInfo
import PrepareAppInfo from "@adaptivestone/framework/services/http/middleware/PrepareAppInfo.js";
PrepareApp info is a special small middleware that generates res.appInfo = . To make sure that all middleware after will use appInfo without checking if it exists. Done for internal usage
Parameters
No parameters
RateLimiter
Rate limiter middleware. Limit amount of request.
import RateLimiter from "@adaptivestone/framework/services/http/middleware/RateLimiter.js";
As rate limited we are using node-rate-limiter-flexible module. Please refer to module documentation for more details
Basic idea of a rate limiter is that we have some weight of the call and some key that has ‘credits’ . Each call consumes ‘credits’ and when it is 0 then the request is blocked.
Same samples - login protection. We can generate rate limiters based on user email and limit for each email only by 5 calls per minute. Or we can construct more complex login that included user IPs, etc
Parameters
Be default rate key generated based on Route, IP and userID. But you can adjust it via config (global) or via middleware parameters
static get middleware() {
return new Map([
[
'POST/login',
[
GetUserByToken,
[
RateLimiter,
{
consumeKeyComponents: { ip: false },
limiterOptions: { points: 5 },
},
],
],
],
]);
}
Rate limited middleware allows you to include request components (req.body) for key generation. Please note that you have no access to req.appInfo.request on this stage
static get middleware() {
return new Map([
['POST/login', [
GetUserByToken,
[RateLimiter,{consumeKeyComponents: { ip: false, request:['email','phone'] }}]
]]
]);
}
You can find default parameters on ‘config/rateLimiter.js’. This parameters used if other parameters not provided.
Rate limiter have multiple backends (memory, redis and mongo). Buy default 'memory' backend activated
RequestLogger
import RequestLogger from "@adaptivestone/framework/services/http/middleware/RequestLogger.js";
Small middleware that logs request info (route, method, status and time).
Logs example:
[middlewareRequestLogger] 2023-01-24T07:00:35.680Z info : Request is [GET] /project/123
[middlewareRequestLogger] 2023-01-24T07:00:35.747Z info : Finished Request is [GET] /project/123. Status: 200. Duration 67 ms
Parameters
No parameters
RequestParser
import RequestParser from "@adaptivestone/framework/services/http/middleware/RequestParser.js";
This is a main middleware to parse request (application/json, multipart/formdata, octet-stream, urlencoded) It is based on the formidable package. After parse data available on tre req.body
Parameters
No parameters
Role
Check user role (user.roles property). If the user has no role then stop request and return error. OR logic (any role will pass user)
import Role from "@adaptivestone/framework/services/http/middleware/Role.js";
Parameters
roles - array of roles to check. OR logic (any role)
static get middleware() {
return new Map([
['POST/someUrl', [
[RoleMiddleware, { roles: ['admin','moderator'] }]
]]
]);
}
StaticFiles
Decrecated and femoved in version 5. Better to use http server (nginx, etc) to handle static files
# nginx sample
server {
root /var/www/application/src/public;
server_name _;
client_max_body_size 64M;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ @backend;
}
location @backend {
proxy_pass http://localhost:3300;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
import StaticFiles from "@adaptivestone/framework/services/http/middleware/StaticFiles.js";
Handle static files. Moslty for the dev purposes. In production better to handle files via webserver.
Parameters
folders - array of folders to hanle files. Required parameter
static get middleware() {
return new Map([
['POST/someUrl', [
[StaticFiles, { folders: ['/var/www/public','/opt/public'] }]
]]
]);
}
Creating own middlewares (or integrate external)
You can create your own middleware. To do that you should extend AbstractMiddleware and provide at least two own functions - description and middleware. Please check code bellow
import AbstractMiddleware from "@adaptivestone/framework/services/http/middleware/AbstractMiddleware.js";
class CustomMiddleware extends AbstractMiddleware {
static get description() {
return "Middleware descrition";
}
// optional
static get usedAuthParameters() {
// Array of parameters that are used for authorization within the middleware
return [
{
name: "Authorization", // name of the parameter
type: "apiKey", // apiKey, http, oauth2, openIdConnect
in: "header", // header, query, cookie
description: this?.description,
},
];
}
// optional
get relatedQueryParameters() {
// Yup object which defines middleware related req.query parameters, allows to validate and get those parameters in req.appInfo.query relative to the route in which the middleware is declared
return yup.object().shape({
limit: yup.number(), // For example
});
}
// optional
get relatedRequestParameters() {
// Yup object which defines middleware related req.body parameters, allows to validate and get those parameters in req.appInfo.request relative to the route in which the middleware is declared
return yup.object().shape({
name: yup.string().required(), // For example
});
}
async middleware(req, res, next) {
// check something
if (!req.body.yyyyy) {
// return and stop processing
return res.status(400).json({});
}
if (this.params.iiii) {
// we can also check all params that we passed during init
}
// go to next one
return next();
}
}
export default CustomMiddleware;