ayEs is a wrapper for the node Express framework that adds some opinions as to how to structure simple web services on an api server. The request flow goes through authorize
-> validate request structure
-> validate request parameters
-> controller middleware
-> response
.
Each stage is supported by library functions with the aim to standardise how service requests are validated and responded to.
-
Authorization is handled using
JWT
and supports adding additional middleware to handle, for example, roles and/or permissions. -
Validation of request structure will check the headers and session state of the request and the requesting user and return errors appropriately on any failed validation.
-
Validation of request parameters is handled using JSON schemes. If required these are provided as part of the endpoint specification and passed to the route builder as part of a route configuration.
-
Controller middleware is an array of functions that make use of the ayES libraries for errors and responses, in order to standardize all service responses.
-
Response is handled by a set of library functions that wrap all responses in a standard format and apply custom headers and response strategies. Again, the intention here is one of standardisation.
- Get started
- Authentication
- Error
- JSON Validator
- Respond
- Request Validator
- Router
- Self documenting endpoints
const ayEs = require('ayes');
/**
* Get a reference to the ayEs router helper lib
*/
const Router = ayEs.router;
/**
* Create a simple express middleware function to deal with login requests.
* PLEASE NOTE: This is not a recommended login strategy for production sites.
*/
const loginController = function loginController(req, res) {
const { username, password } = req.body;
if (password) {
const response = {
id: Math.floor(Math.random() * 6) + 1,
username
};
res.status(200).send(response);
}
};
/**
* The buildRouter helper function takes a configuration object with an
* array of route definitions.
*/
const arouterdata = {
routes: [
{
method: 'post',
path: '/login',
mwares: loginController
}
]
};
/**
* Call the Router#buildRouter function returns an express router with routes
* for each configuration in the routes array.
*/
const authRouter = Router.buildRouter(arouterdata);
Okay, but so far this is just a wrapper for the Express#Router
. It gives us a nice clean format for router definitions, easy to read and reason about. Now let's add more support for the request flow outlined above.
ayEs provides an implementation of authentication by JWT through the Auth
lib. If an endpoint or a set of endpoints grouped into a router instance requires authentication, create and instance of the ayEs#Auth
handler and pass it to the route configuration.
const ayEs = require('ayes');
const Auth = ayes.Auth;
const auth = new Auth(process.env.JWT_SECRET);
// Or use the factory function
const auth = ayEs.returnAuthInstance(process.env.JWT_SECRET);
const routerOptions = {
routes: [
{
auth: auth, //Pass the auth instance here to authenticate just the /me endpoint.
method: 'post',
path: '/me',
mwares: getMe //A controller function
}, ...
]
}
If all routes for a particular router require authentication, simply pass the auth instance on the options.auth
property of the router configuration instead.
const routerOptions = {
auth: auth, //Pass the auth instance here to authenticate all routes by JWT.
routes: [
{
method: 'post',
path: '/me',
mwares: getMe //A controller function
},
{
method: 'put',
path: '/some/other/authenticated/route',
mwares: anotherController
}
]
}
Request to endpoints configured with the ayEs#Auth
lib must send in an Authorization: Bearer <jwt>
header with a valid JWT to access the service.
The lib adds an express middleware function to the route that will handle JWT validation for authentication. This middleware decodes the JWT present in the request's Authorization
header, returning errors on failed validation. If the JWT is present and valid, the JWT payload is added to the express request object as the dtoken
property, so you can access it inside any subsequent middleware.
/**
* With a JWT payload of
* {
* "exp": "2018-03-01T04:49:49.781Z",
* "user": {
* "id": "596dcd99dd19f9227f5a94b1",
* "username": "nectarsoft"
* }
* }
*/
function (req, res) {
const username = req.dtoken.user.username;
console.log(username) // nectarsoft
}
Create ayEs#Auth
instance.
options
- logger: A custom logger for the instance to use.
Decodes the given jwt
string and returns the jwt payload as an object. Throws an error if jwt
is not valid.
Helper function that take the error object returned by Auth#decodeJWT
and parses it to a custom ayEs#Error#AuthError
type with relevant error code and message.
Returns a JWT string with payload
, signed with the given secret key jwtsecret
.
Helper factory function that returns an ayEs#Auth
instances.
Returns the auth middleware function.
This is exposed so that you can add additional authentication middleware if required.
const authMiddleware = [];
authMiddleware.push(auth.generateAuthMiddleWare());
authMiddleware.push(someOtherAuthFunction);
const routerOptions = {
auth: auth, // auth property can be an array of functions.
routes: [ ... ]
}
A set of custom error objects for use in controller middleware to standardise error responses.
All Error
constructors accept a code parameter that can be used to pass in a code string as a non-verbose mechanism to give more specific detail about an error (see error codes).
ayEs error objects are intended for use along side the response library to form a standard error response to api request.
Returns an BadRequestError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 400;
Returns an NotFoundError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 404;
Returns an BadHeaderError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 400;
Returns an AuthError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 401;
This is intended for authentication errors, not authorisation errors (see Error#ForbiddenError)
Returns an ForbiddenError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 403;
Returns an UnauthorizedError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 401;
This is intended for authentication errors, not authorisation errors (see Error#ForbiddenError)
Returns an UnavailableRetryError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 503;
error.retryafter = retryafter;
This error includes a retryafter
property to indicate a wait period until retrying to access the service. Can be used for temporary unavailability, such as cache updates.
Returns an ConflictError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 409;
Returns an DataBaseReturnError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 500;
Returns an JSONValidationError
instance with properties
error.message = message;
error.code = code;
error.httpstatus = 400;
error.info = errorData;
This error is used by the JSONValidator
lib to return errors on validation. The validation error data is passed in the info
property.
There are a set of errors that wrap server errors and are passed the original error as a parameter to the constructor. This error object is then available on the errObj
property.
Returns an DataBaseError
instance with properties
error.message = message;
error.errObj = errobj;
error.code = code;
error.httpstatus = 500;
Returns an ServerError
instance with properties
error.message = message;
error.errObj = errobj;
error.code = code;
error.httpstatus = 500;
Returns an FBError
instance with properties
error.message = message;
error.errObj = errobj;
error.code = code;
error.httpstatus = 500;
Returns an AWSError
instance with properties
error.message = message;
error.errObj = errobj;
error.code = code;
error.httpstatus = 500;
Most times an error is sent back to a client along with a message to give more detail of the cause of the error. A 400 Bad Request
status does not let the client know what was incorrect in the request, so a message is added to clarify, "Email parameter must be a valid email".
There are two good cases when it might be better to replace that message with a code instead.
- We care about bandwidth and want to send less bytes across the wire,
- we want to inform the client app of an error but not a user who might read the response content.
For this purpose atEs
includes the concept of error codes that can be passed to the custom error constructors that give a little more detail about the error cause. These codes are read by the ayEs#response
functions and a custom header, X-Error-Code
, is added with the code. This can be read by developers or client applications for greater granularity of errors without verbose strings over the wire.
See available codes and their meanings here.
TODO Allow for code customisation.
A wrapper for the AJV JSON schema validation library used to validate request parameters.
The idea here is to create an instance of the ayEs#JSONValidator
and register a set of JSON schema that can be used in route configuration. So, given a JSON schema for validating login parameters with an $id
property of postloginin
, such as
const authReqSchema = {
$schema: 'http://json-schema.org/draft-07/schema#',
$id: 'postloginin',
title: 'Login Object',
type: 'object',
properties: {
email: {
title: 'user email',
type: 'string',
format: 'email'
},
username: {
title: 'username',
type: 'string'
},
password: {
title: 'user password',
type: 'string',
minLength: 5,
maxLength: 400
}
},
required: ['username', 'password']
};
we can instantiate the validator either by passing the schema to the constructor
const JSONValidator = ayEs.JSONValidator;
const jv = new JSONValidator(authReqSchema);
or using the JSONValidator#addSchema
instance function
const JSONValidator = ayEs.JSONValidator;
const jv = new JSONValidator();
jv.addSchema(authReqSchema);
Now, this instance can be assigned to the Router#options#jsonv
property and the registered schema can be referenced in a route configuration as validreq: 'postloginin'
{
jsonv: jv,
routes: [
{
method: 'post',
path: '/login',
mwares: loginController,
validreq: 'postloginin' // Reference the schema here
}
]
}
The router library will now add a validation middleware for request parameters using the schema indicated. Any failure against the schema is wrapped in a Error#JSONValidationError
and reported back to the client using Respond#invalidRequest
.
If you use the $ref
property in your JSON schema to reference common definitions in a separate schema file we must pass that to our ayEs#JSONValidator
instance in a slightly different way. This is due to how it is passed to the underlying ajv
library.
First add all the definition schemas into the array of schemas you wish to register with the validator and then wrap the array into an options object.
// schemas/defs.json
{
"$id": "defs", // Id used to reference this schema in other schemas
"definitions": {
"username": {
"title": "Username",
"type": "string",
"pattern": "^[a-zA-Z0-9\\-_.]{3,25}$"
}
}
}
// schemas/user.json
[{
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "GET User Params",
"type": "object",
"required": ["username"],
"properties": {
"username": { "$ref": "defs#/definitions/username" }
}
}]
// routes/user.js
const JSONValidator = ayEs.JSONValidator;
const ds = require('schemas/defs');
const su = require('schemas/user');
su.push(ds);
const jvoptions = { schemas: su };
const jv = new JSONValidator(null, jvoptions);
A wrapper lib for the Express#res.send
function. All responses are standardised. Data returned for a successful request can be wrapped with respond#wrapSuccessData()
before being passed to respond#success()
to be returned to the requestor.
const ayEs = require('ayes');
const respond = ayEs.respond;
// controller for /login route
const loginController = function loginController(req, res) {
const { username, password } = req.body;
if (password) {
const response = {
id: Math.floor(Math.random() * 6) + 1,
username
};
respond.success(res, req, respond.wrapSuccessData(response, req.path));
}
};
// Responds to requester with JSON body
{
"data": {
"id": 2,
"username": "nectarsoft"
},
"path": "/login"
}
Will respond to the requestor with http status 403
.
The last parameter is intended to be a custom ayEs#Error
object and can include message
, httpstatus
and code
properties.
If httpstatus
is present, this will be used in place of 403
.
The code
property is anticipated to be one of 'ayes#Error#codes and if present respond#forbidden
returns an empty body and includes the custom header X-Error-Code
set to the value of code
.
If code
is not present a response body is sent using the message
property from the Error
parameter if present or 'Forbidden' if not.
{
"data": {
"message": "Forbidden" // Or Error.message is present
},
"type": "ForbiddenError"
}
This response is intended to pass back information about any invalid request so a requester can amend and resubmit their failed request.
Any information to be passed to the requester can be attached to the Error.info
property of the last parameter.
So, for example, if used in conjunction with an Error#JSONValidationError
returned by the ayEs#JSONValidator
library we get a response body of the form
response.invalidRequest(response, request ,JSONValidationError);
// Returns a request body of e.g.
{
"data": {
"message": "JSON validation errors were found against schema: postloginin",
"info": [
{
"keyword": "required",
"dataPath": "",
"schemaPath": "#/required",
"params": {
"missingProperty": "password"
},
"message": "should have required property 'password'"
}
]
},
"type": "JSONValidationError",
"code": 400.43
}
Will respond to the requestor with http status 401
and set the WWW-Authenticate
header to Bearer token_path="JWT"
.
The last parameter is intended to be a custom ayEs#Error
object and can include message
, httpstatus
and code
properties.
If httpstatus
is present, this will be used in place of 401
.
The code
property is anticipated to be one of 'ayes#Error#codes and if present respond#notAuthorized
returns an empty body and includes the custom header X-Error-Code
set to the value of code
.
If code
is not present a response body is sent using the message
property from the Error
parameter if present or 'Authorisation error' if not.
{
"data": {
"message": "Authorisation error" // Or Error.message is present
},
"type": "Not_Authorized_Error" // Or Error Constructor name.
}
Will respond to the requestor with http status 404
.
The last parameter is intended to be a custom ayEs#Error
object and can include message
, httpstatus
and code
properties.
If httpstatus
is present, this will be used in place of 404
.
The code
property is anticipated to be one of 'ayes#Error#codes and if present respond#notFound
returns an empty body and includes the custom header X-Error-Code
set to the value of code
.
If code
is not present a response body is sent using the message
property from the Error
parameter if present or 'Resource not found' if not.
{
"data": {
"message": "Resource not found" // Or Error.message is present
},
"type": "Not_Found_Error" // Or Error Constructor name.
}
Will respond to the requestor with http status 501
.
The last parameter is intended to be a custom ayEs#Error
object and can include message
, httpstatus
and code
properties.
If httpstatus
is present, this will be used in place of 501
.
The code
property is anticipated to be one of 'ayes#Error#codes and if present respond#notImplemented
returns an empty body and includes the custom header X-Error-Code
set to the value of code
.
If code
is not present a response body is sent using the message
property from the Error
parameter if present or 'Not Implemented' if not.
{
"data": {
"message": "Not Implemented" // Or Error.message is present
},
"type": "Not_Implemented_Error" // Or Error Constructor name.
}
Will respond to the requestor with http status 307
.
The headers parameter should contain key value pairs of header names and values that will be added to the response. Typically this will include the Location
header with an URL intended for the redirect.
If httpstatus
is present, this will be used in place of 307
.
Will respond to the requestor with http status 500
.
The last parameter is intended to be a custom ayEs#Error
object and can include message
, httpstatus
and code
properties.
If httpstatus
is present, this will be used in place of 500
.
The code
property is anticipated to be one of 'ayes#Error#codes and if present respond#serverError
returns an empty body and includes the custom header X-Error-Code
set to the value of code
.
If code
is not present a response body is sent using the message
"Unexpected Error" if not and setting the type to "Server_Error".
{
"data": {
"message": "Unexpected Error"
},
"type": "Server_Error" // Or Error Constructor name.
}
Will respond to the requestor with http status 200
.
If HttpStatus
parameter is present this will be used in place of 200
.
If WrappedData
if present it is used to build the response object, passing the data as teh data
property of the response an either a path
property, if one exists on the wrappedData
object, or type
property if not.
{
"data": {
"id": 2,
"username": "nectarsoft"
},
"path": "/login"
}
// Or
{
"data": {
"id": 2,
"username": "nectarsoft"
},
"type": "success"
}
If no WrappedData
parameter is passed respond#success
will return an empty body, but still use http status 200
and not 204
.
Will respond to the requestor with http status 503
.
The last parameter is intended to be a custom ayEs#Error
object and can include message
, httpstatus
, retryafter
and code
properties.
This response will set the Retry-After
header to either the number 1 or a value passed in the Error#retryafter
property.
If httpstatus
is present, this will be used in place of 503
.
The code
property is anticipated to be one of 'ayes#Error#codes and if present respond#unavailableRetry
returns an empty body and includes the custom header X-Error-Code
set to the value of code
.
If code
is not present a response body is sent using the message
property from the Error
parameter if present or 'Service temporarily unavailable. Please retry' if not.
{
"data": {
"message": "Service temporarily unavailable. Please retry" // Or Error.message is present
},
"type": "Service_Unavailable_Please_Retry" // Or Error Constructor name.
}
A function to wrap the return data into an options object for the respond#success
function.
const wrappedData = Respond.wrapSuccessData(response, req.path, { stripNull: true });
Respond.success(res, req, wrappedData);
options
- stripNull : Boolean flag to pass to the
respond#success
function to indicate that any value in the data object with a null value should not be returned to the requester.
A peculiar beast used for validating request formats. TODO.
Build an Express Router instance containing endpoints for each of the routes configured in the options.routes
- addOptionsRoute: Boolean flag to indicate whether to add an
options
endpoint that returns a documentation JSON as described here. Defaults tofalse
. - auth: This options takes an instance of ayEs#Auth.If present at this level the authentication middleware will be added to all endpoints configured in the
routes
array. - jsonv: This option takes an instance of the ayEs#JSONValidator. If present, route configurations (in the routes option array) can indicate a json schema registered with the validator to use to validate request parameters.
- routes: An array of endpoint configuration objects. Each object in the array will be used to build and add one endpoint to the returned
Express#Router
. Route configuration options are:- auth: an instance of ayEs#Auth.If present at this level the authentication middleware will be added to only the endpoints configured in this route config. Clearly this will be overidden by any
Auth
instance in theoptions.auth
option (which applies to all routes). If only some routes are required to be authenticated, or ypu wish to use a different authentication instance for any route, nooptions.auth
option should be declared and theauth
options set at the route configuration level for all authenticated routes. - method: The HTTP method for this endpoint.
- mwares: An array of express middleware functions (or single function) for this endpoint. These middleware functions will be added to the router AFTER any authentication and validation middlewares that are generated by the
Auth
and/orJSONValidator
instances in theauth
orjsonv
options. - path: The path for this endpoint. This can be any string excepted as an express route path (see here)
- rxvalid: A list of request validation requirements. TODO Refactor this interface.
- validreq: A string identifier for a JSON schema registered with the
ayEs#JSONValidator
instance set as theoptions.jsonv
option. The schema will be used to validate the request parameters to this endpoint. See JSONValidator for more details. - validres: A string identifier for a JSON schema registered with the
ayEs#JSONValidator
instance set as theoptions.jsonv
option. This is currently only used when generating the documentation JSON for theOPTIONS
route. See JSONValidator for more details.
- auth: an instance of ayEs#Auth.If present at this level the authentication middleware will be added to only the endpoints configured in this route config. Clearly this will be overidden by any
If the flag addOptionsRoute
is set on the options object passed to the Router#buildRouter
function, it will add an OPTIONS
endpoint at the root URL for that router that will return a JSON
containing information about all routes within the router. This will include a data
property that is an object whose property keys are the available paths for the router. Each path will have an array of objects describing each available verb for that path with the following properties:
- verb: HTTP method
- validations: What validations are carried out on the request format. Currently this will list required headers.
- body_schema: A JSON schema for the parameters for this request.
- response: A JSON schema describing the response for this endpoint.
{
"data": {
"/login": [{
"verb": "post",
"validations": {
"headers": {
"Accept": "application/json",
"Content-Type": "application/json"
}
},
"body_schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "postloginin",
"title": "Login Object",
"type": "object",
"properties": {
"email": {
"title": "user email",
"type": "string",
"format": "email"
},
"username": {
"title": "username",
"type": "string"
},
"password": {
"title": "user password",
"type": "string",
"minLength": 5,
"maxLength": 400
}
},
"required": ["username", "password"]
},
"response": {
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "postloginout",
"title": "Login Object",
"type": "object",
"properties": {
"id": {
"title": "user id",
"type": "string"
},
"username": {
"title": "username",
"type": "string"
},
"role": {
"title": "user role",
"type": "number"
}
},
"required": ["id", "username", "role"]
}
}]
},
"path": "/"
}