SuperRoute
Non-pretentious, Mildly-opinionated Express Framework
SuperRoute
is a framework for building readable api routes,
aimed at improving code re-usability, code readability and route documentation.
Main Features:
- Built in request input validation
- Simple yet fine-grained access control management
- Auto-generated documentation
- Simple route versioning
- Inheritance-Based - organize route types by classes
- Simple Error handling
Installation
npm install --save super-route-express
import:
import {SuperRoute} from 'super-route-express';
Usage
SuperRoute
.
Create a new route class that extends You can create classes for specific route types or groups - e.g. routes that require authentication, access control, or share data with the middleware.
import {SuperRoute} from 'super-route-express';
class BasicRoute extends SuperRoute {}
// all created routes will require authentication
class AuthenticatedRoute extends BasicRoute {
authenticate: true;
}
// all created routes will use access control function to block non admin users
class AdminOnlyRoute extends AuthenticatedRoute {
permissions: {
equalOrGreaterThan: 'admin'
}
}
Instantiate Routes
A SuperRoute instance (and it's children's instances) is passed a RouteSettings
configuration object.
Each route has a stack of request handlers that or bound the route as their this
argument when calling mount()
,
it allows access to error handling methods as well as additional instance and class data that might be useful.
See the Api section blow for full options.
const routes: Array<SuperRoute> = [
new AdminOnlyRoute({
path: 'users/new',
verb: 'post',
name: 'new user',
description: 'creates a new user',
bodyParams: [
new BodyParameter('firstName', 'string', 'user first name', true),
new BodyParameter('lastName', 'string', 'user last name', true),
new BodyParameter(
'mobilePhone',
'string',
'user mobile phone',
true,
[{
test: (value: string) => {
return value.length === 10
},
description: 'checks if mobile phone has 10 digits'
}]
),
],
middleware: [
(req: Request, res: Response, next: NextFunction) => {
// ... Some middleware logic
}
]
}),
new AuthenticatedRoute({
path: 'users/read/:id',
verb: 'post',
name: 'new user',
description: 'creates a new user',
authenticate: true,
routeParams: [
new RouteParameter('id', 'mongodb id', true, [{
test: (value: string) => ObjectId.isValid(value),
description: 'id must be a valid mongodb ObjectId'
}]
),
],
middleware: [
// stack req processing middleware here for re-usability
SomeGenericMiddleWare,
SomeMoreGenericMiddleware({configuration: 'some configuration'}),
(req: Request, res: Response, next: NextFunction) => {
// ... Some middleware logic
}
]
}),
]
Mount routes
const apiRouter = Router();
routes.forEach(route => route.mount(router));
Access Control
SuperRoute offers two levels of access control that can be defined in the extending class.
1. Access Control function
Defined by the $$accessControlFunction
property, it is used to limit access to users with defined permissions,
using the route's permissions
settings.
Setting Route Permissions
Use a RoutePermissions object to configure access control for a route instance or extending class with the following options.
-
equalOrGreaterThan
- requester must have a permission level that is equal or greater than the given string as defined by the hierarchy array. -
specific
- requester must have all the given permissions -
merge
- when set to 'and' requester must satisfy both the specific an hierarchical rules.
All properties are optional, but the configuration must contain either equalOrGreaterThan
or specific
.
Example:
{
equalOrGreaterThan: 'admin',
specific: ['specialPermission', 'awesomeDude'],
merge: 'and'
}
will only grant access to admins that also have the specialPermission and awesomeDude permisions
Per-route instance configuration:
const route = new BasicRoute({
path: 'some/path',
verb: 'post',
name: 'some name',
permissions: {
equalOrGreaterThan: 'admin',
specific: ['specialPermission', 'awesomeDude'],
merge: 'and'
}
// ... //
})
Or configure a child class:
class AdminOnlyRoute extends SuperRoute {
permissions: { specific: ['admin'] }
}
Configure Access Control function
- Assign an
AccessControlFunction
to the$$accessControlFunction
in the class definition. When mounted, it will be called with the route's permissions and should return aRequestHandler
. - Use the static method
SuperRoute.checkPermissions
to validate the user's permissions. note thatcheckPermissions
receives a hierarchy Array in which the item with the largest index is the highest in the hierarchy. - Also note that if the route's permissions are not defined by the class or it's instances,
$$accessControlFunction
will not be called.
class AuthenticatedRoute extends SuperRoute {
$$accessControlFunction: AccessControlFunction = (permissions: RoutePermissions) => {
return async (req: Request, res: Response, next: NextFunction) => {
// define heirarchy
const hierarchy = [
'end_user',
'editor',
'admin'
]
let auth;
try {
// check user permissions against route permissions ans heirarchy
auth = SuperRoute.checkPermissions(req.user.permissions, permissions, hierarchy);
} catch (err) {
return next(err);
}
if (!auth) {
// Do some more logic.....
this.handleError(req, res, next, `user does not have sufficient permissions to access ${this.path}`, 403)
} else {
// Do some more logic.....
return next();
}
}
}
}
2. Authentication function
Defined by the $$authenticationFunction
property, it is used to verify that the requester is logged in. it can be used to perform e.g. JWT verification or another login strategy,
or as a form of input validation to verify that the request reaching the middle contains an authenticated user's data.
When mounted, the function is called with the route's permissions definitions.
Example:
class AuthenticatedRoute extends BasicRoute {
authenticate: true;
$$authenticationFunction = (req: Request, res: Response, next: NextFunction) => {
if (req.hasOwnProperty(user)) {
next();
} else {
this.handle(arguments, 'Not Authenticated', 400)
}
}
}
Input Validation
SuperRoute offers both route parameters and body validation. Route parameters and properties of the request's body can be defined in the route settings, serving both the purpose of input validation and route documentation generation.
Input that doesn't meet the defined spec will return a detailed error and 400 status code.
Body Validation
const route = new BasicRoute({
// ...route settings... //
bodyParams: [
new BodyParameter('id', 'string', 'user id', true)
]
})
Pass an array of BodyParameter
to the route's settings with the following arguments:
Name | Type | Default value | Description |
---|---|---|---|
name |
string | - | property name |
type |
ParameterType | 'any' | the expected type of the parameter. if defined, will throw an error if the parameter's type doesn't match |
description |
string | '' | text that will be displayed in the rendered help output |
required |
boolean | true | if true, will throw an error when the property is missing |
additionalTests |
RequestParameterTestFunction[] | [] | an array of additional test functions and their description |
BodyParameter
(as well as RouteParameter
) can be defined with additional validations (in addition to the type checking)
new BodyParameter('mobilePhone', 'string', 'user mobile phone', true, [
{
test: (value: string) => {
return value.length === 10
},
description: 'mobile phone must have 10 digits'
}
])
All validation errors are combined and returned.
Route Parameters Validation
const route = new BasicRoute({
// ...route settings... //
bodyParams: [
new RouteParameter('id', 'user id', true)
]
})
Pass an array of RouteParameter
to the route's settings with the following arguments:
Name | Type | Default value | Description |
---|---|---|---|
name |
string | - | property name |
description |
string | '' | text that will be displayed in the rendered help output |
required |
boolean | true | if true, will throw an error when the property is missing |
additionalTests |
RequestParameterTestFunction[] | [] | an array of additional test functions and their description |
Route Parameters can also have additional validations same as body parameters
Error Handling
this.handle()
Handling with Middleware functions are all bound to the route as their this
argument, so it is possible to call this.handle()
from any of the middleware functions, with following parameters:
Name | Type | Default value | Description |
---|---|---|---|
middlewareArgs |
IArguments | - | req, res, next from the express middleware function |
errorOrMessage |
string | Error | Error object, or an error message string |
statusCode |
number | 500 | html response status code |
respondWith? |
string | - | optional custom error message to send as response |
log |
boolean | false |
log the error to the console if true |
redirect |
string | false |
redirect url passed to the error handler |
options? |
object | - | options object to pass to a custom error handler |
It will construct a RouteError
object and pass it to next()
.
It's also possible to just pass a vanilla Error object to next, but handling with RouteError
Generate Route Documentation
Route information, including body / route parameters and their validation rules can be exported.
Export to markdown
call toMarkdown()
method on any route to generate documentation.
Optionally, on an array of routes:
fs.writeFileSync('routes.md', RoutesArray.map(route => route.toMarkdown()).join('<br><br>'))
Here's an example of a route that uses all features available for documentation.
You can see the resulting markdown output here.
View Documentation in terminal
It's also possible to enable making and OPTIONS
http request to a route and receive a text output of the route's documentation,
which may be helpful during development.
Expample:
import {SuperRoute} from "./SuperRoute";
function shouldShowHelp() {
return process.env.test === 'true'
}
class RouteWithHelp extends SuperRoute {
showHelp = (self: RouteWithHelp) => {
return process.env.test === 'true';
}
}
// Or
const route = new BasicRoute({
//... route settings ...//
showHelp: true // or false
})
curl --request OPTIONS localhost:8080/path/to/route
Route Versioning
SuperRoute can also VersionedMiddleware
objects to handle route versioning.
See version-router-express for further configuration details.
Example:
configure route with versioned routes
app.use(VersionedRouter.ExtractVersionFromHeader('App-version'));
// .... server logic .... //
new BasicRoute({
path: 'versioned_route',
verb: 'get',
name: 'versioned route',
versionedMiddleware: [
{
version: '1.0.0',
default: false,
middleware: [
(req: Request, res: Response, next: NextFunction) => {
console.log('route 2 function 1')
next()
},
(req: Request, res: Response, next: NextFunction) => {
console.log('route 2 function 2')
res.send({route: '1'})
},
]
},
{
version: '>=1.2.0 <2.0.0',
default: false,
middleware: [
(req: Request, res: Response, next: NextFunction) => {
console.log('route 2 function 1')
next()
},
(req: Request, res: Response, next: NextFunction) => {
console.log('route 2 function 2')
res.send({route: '2'})
},
]
},
{
version: '2.0.0',
default: true,
middleware: [
(req: Request, res: Response, next: NextFunction) => {
console.log('route 3 function 1')
next()
},
(req: Request, res: Response, next: NextFunction) => {
console.log('route 3 function 2')
res.send({route: '3'})
},
]
}
]
})
make requests with custom header
curl -H 'App-version: 1.0.0' -X GET localhost:8081/versioned_rout
## ==> '{route: '1'}'
curl -H 'App-version: 1.1.0' -X GET localhost:8081/versioned_rout
## will resolve to default
## ==> '{route: '3'}'
curl -H 'App-version: 1.3.0' -X GET localhost:8081/versioned_rout
## ==> '{route: '2'}'
API Reference
Interface: RouteSettings
Configuration Object for SuperRoute instance
const route = new SuperRoute({
path: 'some/path',
verb: 'post',
name:'some name'
// ....
})
Name | Type | Description | Required |
---|---|---|---|
path |
ExpressHttpVerb | Http Verb | true |
verb |
string | Route path | true |
name |
string | Route Name | true |
comments |
string | Additional comments for documentation | fasle |
description |
string | Route description | fasle |
middleware |
RequestHandler[] | Route Middleware | fasle |
versionedMiddleware |
VersionedMiddleware[] | An Array of VersionedMiddleware instances - see https://www.npmjs.com/package/version-router-express | fasle |
bodyParams |
BodyParameter[] | An Array of BodyParameter instances, defining required and/or optional parameters for request body as well as input validation tests | fasle |
routeParams |
RouteParameter[] | An Array of RouteParameter instances, defining the types and validation rules for route parameters | fasle |
authenticate |
boolean | When true, will mount the authentication function as middleware before other routes | fasle |
permissions |
RoutePermissions | A RoutePermissions object for use with the package's standard access control function | fasle |
responseContentType |
string | response content type - used to set headers | false |
errorHandlerOptions |
ErrorHandlerOptions | Options that will be passed to the global error handler | |
redirectOnError |
string | Optional redirect when error is handled by the route | false |
showHelp |
boolean or ( self : SuperRoute) => boolean
|
When set to true, will return an ascii output of the requested route's information when called with the OPTIONS http method. Example: curl --request OPTIONS https://localhost:8080/path/to/my/route
|
fasle |
responseFormat |
string | response body type - used for automated testing | false |
responseReturnType |
SuccessResponse | response format used for documentation | false |
Class: SuperRoute
Base Class for an express route super route Middleware order:
- Authentication function
- Access control
- Route Parameters validation
- Route Parameters validation
- Route Specific middleware defined in the middleware or versioned middleware arrays
Implements
Methods
handle
▸ handle(middlewareArgs
: IArguments, errorOrMessage
: string | Error | RouteError, statusCode?
: number, respondWith?
: string, log?
: boolean, redirect?
: string | false
, options?
: { [key: string]: any; }): void
handles errors in the route's logic.
errors can be channeled to a custom error handler specific to the route by defining $$errorHandler by default, a RouteError object containing route and request data will be created and passed to next(err)
Parameters:
Name | Type | Default value | Description |
---|---|---|---|
middlewareArgs |
IArguments | - | req, res, next from the express middleware function |
errorOrMessage |
string | Error | RouteError |
statusCode |
number | 500 | html response status code |
respondWith? |
string | - | optional custom error message to send as response |
log |
boolean | false |
log the error to the console if true |
redirect |
string | false |
- |
options? |
object | - | options object to pass to a custom error handler |
Returns: void
checkPermissions
▸ Static
checkPermissions(userPermissions
: string | string[], permissions
: RoutePermissions, hierarchy
: string[]): boolean
checks if the user has the permissions defined in the permissions object and according to the defined hierarchy For use with an access control function.
Parameters:
Name | Type |
---|---|
userPermissions |
string |
permissions |
RoutePermissions |
hierarchy |
string[] |
Returns: boolean
mount
▸ mount(router
: Router): void
mounts the route on a router instance or express app
Parameters:
Name | Type |
---|---|
router |
Router |
Returns: void
Defined in: src/SuperRoute.ts:159
toMarkdown
▸ toMarkdown(): string
Generates markdown documentation for hte route
Returns: string
Defined in: src/SuperRoute.ts:418
DefaultErrorHandler
▸ Static
DefaultErrorHandler(err
: RouteError
RouteErrorI, req
: Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>, res
: Response<any, Record<string, any>>, next
: NextFunction): void
a default error handler to mount as the last middleware of the app
Parameters:
Name | Type |
---|---|
err |
RouteError RouteErrorI |
req |
Request |
res |
Response |
next |
NextFunction |
Returns: void
Class: BodyParameter
Defines a parameter expected to be present in the request's body
constructor
new BodyParameter(name
: string, type?
: ParameterType, description?
: string, required?
: boolean, additionalTests?
: RequestParameterTestFunction[]): BodyParameter
Constructs a BodyParameter instance
Parameters:
Name | Type | Default value | Description |
---|---|---|---|
name |
string | - | property name |
type |
ParameterType | 'any' | the expected type of the parameter. if defined, will throw an error if the parameter's type doesn't match |
description |
string | '' | text that will be displayed in the rendered help output |
required |
boolean | true | if true, will throw an error when the property is missing |
additionalTests |
RequestParameterTestFunction[] | [] | an array of additional test functions and their description |
nullable |
boolean | false | when true, allows a required parameter to be null, ignoring it's type definition |
Class: RouteParameter
Defines a parameter expected to be present in the request's route
constructor
new RouteParameter(name
: string, description?
: string, required?
: boolean, additionalTests?
: [RequestParameterTestFunction](#Interface: RequestParameterTestFunction)[]): RouteParameter
Constructs a BodyParameter instance
Parameters:
Name | Type | Default value | Description |
---|---|---|---|
name |
string | - | property name |
description |
string | '' | text that will be displayed in the rendered help output |
required |
boolean | true | if true, will throw an error when the property is missing |
additionalTests |
RequestParameterTestFunction[] | [] | an array of additional test functions and their description |
Interface: RequestParameterTestFunction
A test function and test description for testing request parameters
new BodyParameter('age', 'number', 'user age', true, [
{
test: (value) => value > 18,
description: 'User must be over 18'
}
])
Name | Type | Description |
---|---|---|
test |
test(value: any): boolean |
a test function that receives the parameter value as an argument and returns a boolean |
description |
string | an optional description of the test that will be displayed in the error output in case the function returns false |
Interface: AccessControlFunction
Access control function for SuperRoute settings When mounted, it will be called with the route's permissions and should return a RequestHandler
▸ AccessControlFunction(permissions
: RoutePermissions): RequestHandler
Parameters:
Name | Type |
---|---|
permissions |
RoutePermissions |
Interface: RoutePermissions
Access Control configuration for a route instance or extending class
-
equalOrGreaterThan
- requester must have a permission level that is equal or greater than the given string as defined by the hierarchy array. -
specific
- requester must have all the given permissions -
merge
- when set to 'and' requester must satisfy both the specific an hierarchical rules.
Example:
{
equalOrGreaterThan: 'admin';
specific: ['specialPermission', 'awesomeDude'];
merge: 'and'
}
will only grant access to admins that also have the specialPermission and awesomeDude permisions
Interface: ErrorHandlerOptions
Indexable
Properties
log
• Optional
log: boolean
redirectOnError
• Optional
redirectOnError: string
Class: RouteError
A SuperRoute error with additional data
-
Error
↳ RouteError
constructor
+ new RouteError(message
: string, statusCode
: number, redirect?
: null
| string | false
, log?
: boolean): RouteError
Constructs a RouteError instance
Parameters:
Name | Type | Default value | Description |
---|---|---|---|
message |
string | - | Error message |
statusCode |
number | - | Http status code |
redirect |
null string false
|
false | tells the error handler if it should redirect the request |
log |
boolean | false | tells the error handler to log the error - use it to override the default logging behaviour for specific errors when needed. |
Returns: RouteError
Properties
logError
• logError: boolean
message
Error message
• message: string
Inherited from: Error.message
redirect
Tells the error handler if it should redirect the request
• redirect: string | false
requestPath
The path of the request that invoked the route that where the error was thrown
• Optional
requestPath: string
response
Optional response to replace the error message when sent to the user
• Optional
response: string
route
The path of the route where the error was thrown
• Optional
route: string
statusCode
Response status code
• statusCode: number
Methods
handle
Handles a RouteError with the data of the given route. Example:
new RouteError('SomeError', 400, null, true).handle(this, req, res, next);
▸ handle(route
: SuperRoute, req
: Request, res
: Response, next
: NextFunction): void
Parameters:
Name | Type |
---|---|
route |
any |
req |
Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>> |
res |
Response<any, Record<string, any>> |
next |
NextFunction |
Returns: void
Defined in: src/RouteError.ts:66
respondWith
Attach a custom response to the RouteError object
▸ respondWith(response
: string): RouteError
Parameters:
Name | Type |
---|---|
response |
string |
Returns: RouteError
Defined in: src/RouteError.ts:61
FromError
Generate a RouteError instance from a vanilla Error
instance.
▸ Static
FromError(err
: Error, statusCode
: number, redirect?
: null
| string | false
, log?
: boolean): RouteError
Parameters:
Name | Type | Default value |
---|---|---|
err |
Error | - |
statusCode |
number | - |
redirect |
null string false
|
false |
log |
boolean | false |
Returns: RouteError
Type aliases
ExpressHttpVerb
Available http verbs for super-route settings object
Ƭ ExpressHttpVerb: "get"
| "post"
| "put"
| "head"
| "delete"
| "options"
| "trace"
| "copy"
| "lock"
| "mkcol"
| "move"
| "purge"
| "propfind"
| "proppatch"
| "unlock"
| "report"
| "mkactivity"
| "checkout"
| "merge"
| "m-search"
| "notify"
| "subscribe"
| "unsubscribe"
| "patch"
| "search"
| "connect"
ParameterType
Ƭ ParameterType: "string"
| "number"
| "boolean"
| "object"
| "array"
| "parsableDateString"
| "null"
| "any"
Optional types for a body parameter
Defined in: src/RequestParameters.ts:93
SuccessResponse
response options
Ƭ SuccessResponse: "message"
| "Array"
| "object"
| "file"