API json responses done easy


Keywords
express, api, json, co, generators, promises, generator, promise, error handler, exception, async, await, async/await
License
MIT
Install
npm install express-deliver@0.0.9

Documentation

express-deliver

npm Codecov branch Travis branch Code Climate

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
*/

License

MIT