@mazeltov/middleware
v1.0.8
Published
Middleware for Mazeltov project
Downloads
3
Readme
Reusable Middleware
The repo contains re-usable middleware factories that can be used to simplify API building
Request Lifecycle
- Values are copied with useArgs from query, params, and body into an object called
req.args
- Args are validated using validateArgs
- Args are then consumed by a model using consumeArgs which:
- Puts the successful result in res.locals.result
- Or puts the caught error in res.locals.error
- viewJSON, viewTemplate is used to render the result or error as JSON or HTML
Basic Example
This is the most barebones example usage to make it clear what is going on without access control.
const {
useArgs,
validateArgs,
consumeArgs,
viewJSON,
} = require('@mazeltov/middleware');
const {
validate: {
withLabel,
isNotEmpty,
isString,
isOneOf,
},
} = require('@mazeltov/util');
const router = require('express').Router()
const logger = require('@mazeltov/logger')('myApp/route/foo');
router.get('/foo/:id', [
useArgs({
params: ['id'],
query: [
// you can alias args (source -> alias)
['motive', 'fooMotive'],
'isImportant'
],
// static args are like defaults
static: {
activated: false,
},
// each middleware accepts a logger but the default is global console
logger,
}),
validateArgs({
// This is purely illustrative. You actually want to put validation in model
// as validateGet, validateCreate and pass as validator key.
validate: {
fooMotive: withLabel('Foo motive', [
isNotEmpty,
isString,
[isOneOf, ['love', 'honor']],
]),
},
logger,
}),
consumeArgs({
// here you would replace with your model method like foo.get
consumer: (args) => args,
logger,
}),
viewJSON({ logger }),
]);
Real Usage
Here is a complete endpoint using authentication and authorization that produces JSON
const {
requireAuth,
canAccess,
useArgs,
validateArgs,
consumeArgs,
viewJSON,
} = require('@mazeltov/middleware');
const logger = require('@mazeltov/logger')('My logger label');
const router = require('express').Router();
const db = require('some-db');
const {
personRoleModel,
} = require('@mazeltov/model')({ db }, [
'personRole',
]);
// require your own model
const bookModel = /* ... */
router.get('/book/:id', [
requireAuth({
publicKeyPem: 'PUBLIC KEY PEM TEXT HERE',
logger,
}),
canAccess({
personRoleModel,
checkMethod: bookModel.canGet,
bypassClient: true,
logger,
}),
useArgs({
params: ['id'],
logger,
}),
validateArgs({
validate: {
id: withLabel('ID', [
isNotEmpty,
isString,
]),
},
logger,
}),
consumeArgs({
consumer: bookModel.getBook
logger,
}),
viewJSON(),
]);
Standard Response Patterns
Each of the examples below use the resource racoon
as an example.
Global Rules:
- Singular MUST be used! It is not GET /racoons, it is GET /racoon/list
- All responses return an object with a result and error key.
- All messages and errors should be human/customer readable
- No programmer talk (error, exception, record, uncaught, boolean, array)
- The following vernacular MUST be used in any client facing messages for each action
- "remove" instead of "delete" or "destroy"
- YES : The person you are trying to remove could not be found
- NO: The person could not be deleted, Error cannot destroy person
- "find" instead of "get" or "locate"
- YES: The person could not be found
- NO: Cannot get person
- "update" is fine but should be supported with detailed list of field errors
- "create" is fine but should be supported with detailed list of field errors
- "remove" instead of "delete" or "destroy"
Result property: Object, Array, null
- The
result
property is- An object for single resource records for GET, PUT, PATCH
- An object for a summary of actions
numRemoved
for DELETE endpoints
- An array of objects for listing multiple records
- An empty array when there are no records in a list
- null otherwise (not found, conflict, gone)
Error property: Object
The error
property is always extant and is an object. The lookup
and list
sub-properties are always an object and array respectively (even when empty).
- A lookup with error keys that can be:
- The machine readable name of form field that threw the error
- Any one of these error keys to indicate a general error
- _badRequest
- _unauthorized
- _forbidden
- _notFound
- _conflict
- _gone
- _unprocessableEntity
- _serverError
- _timeout
- A list of errors ordered by key in ascending alphabetical order
- General errors prefixed with underscores will then be hoisted on top
- Must be an array of objects
- Each object must have:
- A key that matches what is in the lookup
- A message
- Optionally (but recommended if possible) an appropriate help link
GET /racoon/:id
Found (Status 200)
{
result: { id: 12 },
error: {
lookup: {},
list: {},
},
}
Not Found (Status 404)
{
"result": null,
"error": {
"lookup": {
"_notFound": true,
},
"list": [
{
"key": "_notFound",
"message": "The racoon you are looking for could not be found",
"help" : "https://help.example.com/racoon/#get-a-racoon",
}
],
},
}
PUT /racoon/:id
Successful Update (Status 200)
This should return the new, updated record under result
{
"result": {
id: 12,
name: 'New racoon name'
},
"error": {
"lookup": {},
"list": {},
},
}
No Extant Record (Status 409 "Conflict")
When no record exists to be updated, a 409 should be thrown
{
"result": null,
"error": {
"lookup": {
"_conflict": true,
},
"list": {
{
"key" : "_conflict",
"message": "The racoon you are trying to update could not be found",
"help": "https://help.example.com/racoon/#update-a-racoon",
},
},
},
}
Record Was Deleted (Status 410 "Gone")
Whenever possible to detect a record was deleted, a 410 should be sent back. This is usually the case if soft-deletes are used on the record, but otherwise a 409 is acceptable.
This should look like the example above, but replace the _conflict key with _gone.
GET /racoon/list
When getting one or more of a resource, the /list
suffix is used in the path. The singular name of the entity is always used in the URI (racoon, not racoons!). Additionally:
- Pagination MUST be supported and not bypassable via API.
The /*/list endpoints MUST accept:
- A
page
query parameter defaulting to 0 - A
limit
query parameter defaulting to 12
The /*/list endpoints MUST additionally provide:
- currentPage which shows the page of the result
- nextPage which is currentPage + 1, but can be null if on the last page
- prevPage which is currentPage - 1, but can be null if on the first page
- total showing the total number of records across all pages
Found Records (200)
{
"result": [
{ id: 1, name: 'Rocky' },
{ id: 2, name: 'Rocky' },
{ id: 3, name: 'Rocky' },
{ id: 4, name: 'Rocky' },
{ id: 5, name: 'Rocky' },
{ id: 6, name: 'Rocky' },
{ id: 7, name: 'Rocky' },
{ id: 8, name: 'Rocky' },
{ id: 9, name: 'Rocky' },
{ id: 10, name: 'Rocky' },
{ id: 11, name: 'Rocky' },
{ id: 12, name: 'Rocky' },
],
"currentPage": 0,
"nextPage": 1,
"prevPage": null,
"total": 32,
"error": {
"lookup": {},
"list": {},
},
}
No Records (Status 200 "Found")
A 200 status code is still returned even when no records are found
204 and 206 are not used because the content is not considered missing or partial. The response is considered complete for our purposes.
{
"result": [],
"currentPage": 0,
"nextPage": null,
"prevPage": null,
"total": 0,
"error": {
"lookup": {},
"list": {},
},
}
DELETE /racoon/:id
Deleted One (Status 200 "Found")
Nothing to Delete (Status 409 "Conflict")
Nothing to Delete (Status 410 "Gone")
{
"result": null,
"error" : {
"lookup" : { "_gone": true },
"list": [
"message" : "The racoon you are trying to remove"
],
}
}
Full Error Example
A full example with fields throwing a 400 error is below
{
"error": {
"lookup": {
"username": true,
"email": true,
},
"list": [
{
"key": "username",
"message": "Username is required",
"help": "https://help.example.com/create-account#username",
},
{
"key": "email",
"message": "Email is required",
"help": "https://help.example.com/create-account#email",
},
{
"key": "email",
"message": "Email must be a valid email",
"help": "https://help.example.com/create-account#email",
},
]
}
}
Roadmap Items
Replace redis cache middleware and files with generic caching middleware that accepts a cacheService
In @mazeltov/model implement access token scopes in tokenGrantModel and clientRole model, Incorporate this scope handling in the canAccess middleware
- Idea for implementation: scopes relate to multiple permissions when a scope is attached to the JWT we gather distinct permissions from scopes and get the intersection of these permissions with the user permissions.