express-deliver
v1.0.0
Published
API json responses done easy
Downloads
109
Maintainers
Readme
express-deliver
Make API json responses easily using generators (or async/await) and promises.
Motivations
Tired of writting the same json responses and error catches everywhere across the express app controllers.
Example
In a normal API json-based app, a route controller could look like this:
app.get('/',function(req,res){
getAsyncList()
.then(list=>{
res.send({status:true,data:list})
})
.catch(err=>{
res.status(500)
res.send({status:false,error:err.message})
})
})
The same behaviour using expressDeliver
becomes:
app.get('/',function*(){
return yield getAsyncList()
})
//or using ES7 async/await
app.get('/',async function(){
return await getAsyncList()
})
It allows you to write simpler controllers, with easy to read & write 'synchronous' code thanks to generators or ES7 async/await
Getting started
npm install --save express-deliver
yarn add express-deliver
Initialize your expressDeliver
app:
const expressDeliver = require('express-deliver')
const express = require('express')
const app = express()
//It should be before your first middleware
expressDeliver(app)
//This is your route controller (notice the *)
app.get('/',function*(){
return 'hi'
})
// --> 200 {"status":true,"data":"hi"}
//It should be after your last middleware
expressDeliver.errorHandler(app)
Returning and yielding
Everything you return inside the generator function ends in the json response body. Some controller examples:
function*(){
return {lastVersion:15}
}
// --> 200 {"status":true,"data":{"lastVersion":15}}
function*(req){
// getUser function returns a promise with user object
return yield getUser(req.query.userId)
}
// --> 200 {"status":true,"data":{"name":"Alice"}}
function*(){
// These promises are resolved in parallel
return yield {
a: Promise.resolve(1),
b: Promise.resolve(2),
}
}
// --> 200 {"status":true,"data":{"a":1,"b":2}}
function*(){
let [a,b] = yield [ Promise.resolve(1), Promise.resolve(2) ]
return a + b
}
// --> 200 {"status":true,"data":3}
See more yield options in co documentation
Async/await
To use this feature you will need node >=7.6
The same pattern used with generators can be used here, with the exception that you can only await
for promises. So if you want to resolve promises in parallel you will need to use Promise.all for arrays or bluebird methods for something more sofisticated like .props()
.
The same examples as before, but using async/await:
async function(){
return {lastVersion:15}
}
// --> 200 {"status":true,"data":{"lastVersion":15}}
async function(req){
// getUser function returns a promise with user object
return await getUser(req.query.userId)
}
// --> 200 {"status":true,"data":{"name":"Alice"}}
async function(){
// These promises are resolved in parallel
return await bluebird.props({
a: Promise.resolve(1),
b: Promise.resolve(2),
})
}
// --> 200 {"status":true,"data":{"a":1,"b":2}}
async function(){
let [a,b] = await Promise.all([ Promise.resolve(1), Promise.resolve(2) ])
return a + b
}
// --> 200 {"status":true,"data":3}
In this document you will find all the examples written with generators, but the behaviors all are almost the same using async/await (just changing * for async and yield for await).
Promises
You can also return plain old promises. Your controller/middleware function name should ends in 'deliver'
. Examples:
app.get('/',function deliver(){
return Promise.resolve('hi')
})
/*
200 {"status":true,"data":"hi"}
*/
app.get('/user',function getUserDeliver(){
return getUser(req.query.userId)
.then( user => user.name )
})
/*
200 {"status":true,"data":"Alice"}
*/
Synchronous response
You can deliver responses with data you already have (or can have synchronously). Your controller/middleware function name should ends in 'deliverSync'
. Examples:
app.get('/',function deliverSync(){
return config
})
/*
200 {"status":true,"data":{ "key": 1234 }}
*/
app.get('/user',function getUserDeliverSync(){
return this.user.name
})
/*
200 {"status":true,"data":"Alice"}
*/
Using as middleware
If you call next()
, no response is generated, the return value is ignored.
Also res.locals
is used as the context (this
) of the generator.
app.use(function*(req,res,next){
res.setHeader('x-session',yield getSession(req.body.token))
this.user = yield getUser(req.query.userId)
// same as res.locals.user = ..
next()
})
//Later in other controller of the same request
app.get('/',function*(){
return this.user.name
})
/*
200 {"status":true,"data":"Alice"}
*/
Error handling
Every error thrown in the request middleware chain gets caught by expressDeliver
error handler (also async ones by using domains).
All errors caught are converted to error-like custom exceptions, so they can hold more options useful in the responses, like error code or extra data.
app.get('/',function(req,res,next){
throw new Error('My error')
// same as next(new Error('My error'))
})
/* --> 500
{
status:false,
error:{
code:1000,
message:'Internal error',
data:'Error: foo'
}
}
*/
Custom Exceptions
Your custom exceptions are defined inside an ExceptionPool
instance.
Example exceptionPool.js
file:
const {ExceptionPool} = require('express-deliver')
// or const ExceptionPool = require('express-deliver').ExceptionPool
const exceptionPool = new ExceptionPool({
MyCustomError: {
code:2001,
message:'This my public message',
statusCode:412
},
MyError: {
code:4000,
message:'My message',
}
})
//Optional post-creation exception adding
exceptionPool.add('OtherError',{
code:1100,
message:'Other error message'
})
module.exports = exceptionPool
These are passed to expressDeliver
options:
expressDeliver(app,{
exceptionPool: require('./exceptionPool.js')
})
Throw example in a service:
//service.js
const {exception} = require('./exceptionPool')
exports.getData(){
throw new exception.MyCustomError()
}
The exceptionPool
object is available in res
:
app.get('/',function(req,res){
throw new res.exception.MyCustomError()
})
/* --> 412
{
status:false,
error:{
code:2001,
message:'This my public message'
}
}
*/
The first argument of the contructor ends in error.data
property of the response:
app.get('/',function(req,res){
throw new res.exception.MyCustomError({meta:'custom'})
})
/* --> 412
{
status:false,
error:{
code:2001,
message:'This my public message',
data: {
meta: 'custom'
}
}
}
*/
Converting errors to exceptions
Generic errors thrown by third parties can be converted automatically to your custom exceptions.
Without conversion we got:
app.get('/',function*(){
return fs.readdirSync('/invalid/path')
})
/* --> 500
{
status: false,
error: {
code: 1000,
message: "Internal error",
data: "Error: ENOENT: no such file or directory, scandir '/invalid/path'"
}
}
*/
To enable exception conversion you can define your exceptions with a conversion function. This function gets the generic error as first parameter and should return a boolean.
For example:
// Previously in our app
// added to expressDeliver options also)
exceptionPool.add('NoSuchFile',{
code:2004,
statusCode:400,
message:'No such file or directory',
conversion: err => err.code=='ENOENT'
})
//Route controller
app.get('/',function*(){
return fs.readdirSync('/invalid/path')
})
/* --> 400
{
status: false,
error: {
code: 2004,
message: "'No such file or directory"
}
}
*/
// You can customize the response error.data with:
exceptionPool.add('ParsingError',{
code:2005,
message:'Cannot parse text',
conversion:{
check: err => err.message.indexOf('Unexpected token')===0,
data: err=> 'Parsing problem on:' + err.message
}
})
Error response options
Responses object can contain more info about errors:
expressDeliver(app,{
printErrorStack: true, //Default: false
printInternalErrorData: true //Default: false *
})
//Default error response:
{
status: false,
error: {
code: 1000,
message: "Internal error"
}
}
//With both set to true:
{
status: false,
error: {
code: 1000,
message: "Internal error",
data: "ReferenceError: foo is not defined",
stack: "ReferenceError: foo is not defined at null.<anonymous> (/home/eduardo.hidalgo/repo/own/file-manager/back/app/routes.js:13:9) at next (native) at onFulfilled (/home/eduardo.hidalgo/repo/own/express-deliver/node_modules/co/index.js:65:19) at .."
}
}
Both should be set to false
for production enviroments.
* This documentation shows the responses as if printInternalErrorData
were true
by default
Error logging
This example is using debug for logging to console:
const debug = require('debug')('error')
expressDeliver(app,{
onError(err,req,res){
debug(err.name,err)
}
})
//Example console output:
/*
app:error InternalError { ReferenceError: foo is not defined
at null.<anonymous> (controller.js:13:9)
at ...
name: 'InternalError',
code: 1000,
statusCode: 500,
data: 'ReferenceError: foo is not defined',
_isException: true } +611ms
*/
Using Routers
You can use routers (or sub-apps) as usual, just remember to initialize it with expressDeliver
. For example:
// authRouter.js
const router = express.Router()
expressDeliver(router)
router.get('/',function*(){
return 'hi from auth'
})
// errorHandler is not necesary, main app will manage it
module.exports = router
// main.js
const authRouter = require('./authRouter.js')
const app = express()
expressDeliver(app)
app.use('/auth',authRouter)
expressDeliver.errorHandler(app)
/* Request to /auth --> 200
{
"status": true,
"data": "hi from auth"
}
*/
The exceptions loaded into the router
are only available within its controllers, as well as any other exceptions from parent apps or routers.
Any other option defined in routers are ignored in favor of main app options.
Customizing responses
By default the responses look like:
// Success:
{
"status":true,
"data":[your-data]
}
//Errors:
{
"status":false,
"error":{
"code":[error-code],
"message":[error-message],
"data":[optional-error-data],
}
}
Using ResponseData
Same as exception
, ResponseData
is available from the package and controller res
. It can be used to extend the response properties.
const {ResponseData} = require('express-deliver')
// or const ResponseData = require('express-deliver').ResponseData
app.get('/',function*(req,res){
// res.ResponseData === ResponseData
return new res.ResponseData({
meta:'custom'
})
})
/* --> 200
{
"status": true,
"meta": "custom"
}
*/
You can remove the status
from the response (with this option set to false, response data is not converted to object and you can send any other type):
function*(req,res){
return new res.ResponseData('my text response',{appendStatus:false})
}
/* --> 200
my text response
*/
Transform responses option
In options parameter, you can set transformation for both success and error responses
Example:
expressDeliver(app,{
transformSuccessResponse(value,options,req){
return {result:value}
},
transformErrorResponse(error,req){
return {
error:error.code,
where:req.url
}
}
})
// Success:
{
"result":[your-data]
}
//Errors:
{
"error":[error-code],
"where":[request-url]
}
Corner cases
No json response
You should use this approach if you want to use coroutine capabilities but not ending in a json response:
function*(req,res,next){
next('ignore') //This tells expressDeliver to ignore coroutine result
res.render('home',yield getData())
}
/* --> 200
<html><head>...
*/
Empty return
function*(){
//Nothing here
}
//or
function*(){
let a
return a
}
/* --> 200
{
"status": true
}
*/
Sending a response before returning something
Resolving the response before returning something in generator, throws an error (HeadersSent
) caughtable in onError
logging option :
function*(req,res){
res.send('my previously sent data')
return {foo:20}
}
expressDeliver(app,{
onError(err,req,res){
console.log(err.name) //HeadersSent
console.log(err.data) //{status:true,data:{foo:20}}}
}
})
/* --> 200
my previously sent data
*/