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
The same behaviour using expressDeliver
becomes:
app //or using ES7 async/await app
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-deliveryarn add express-deliver
Initialize your expressDeliver
app:
const expressDeliver = const express = const app = //It should be before your first middleware //This is your route controller (notice the *)app// --> 200 {"status":true,"data":"hi"} //It should be after your last middlewareexpressDeliver
Returning and yielding
Everything you return inside the generator function ends in the json response body. Some controller examples:
{ return lastVersion:15}// --> 200 {"status":true,"data":{"lastVersion":15}} { // getUser function returns a promise with user object return }// --> 200 {"status":true,"data":{"name":"Alice"}} { // These promises are resolved in parallel return a: Promise b: Promise }// --> 200 {"status":true,"data":{"a":1,"b":2}} { let ab = Promise Promise 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:
{ return lastVersion:15}// --> 200 {"status":true,"data":{"lastVersion":15}} { // getUser function returns a promise with user object return await }// --> 200 {"status":true,"data":{"name":"Alice"}} { // These promises are resolved in parallel return await bluebird}// --> 200 {"status":true,"data":{"a":1,"b":2}} { let ab = await Promiseall Promise Promise 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/*200 {"status":true,"data":"hi"}*/ app/*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/*200 {"status":true,"data":{ "key": 1234 }}*/ app/*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//Later in other controller of the same requestapp/*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/* --> 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 = // or const ExceptionPool = require('express-deliver').ExceptionPool const exceptionPool = MyCustomError: code:2001 message:'This my public message' statusCode:412 MyError: code:4000 message:'My message' //Optional post-creation exception addingexceptionPool moduleexports = exceptionPool
These are passed to expressDeliver
options:
Throw example in a service:
//service.jsconst exception = exports throw
The exceptionPool
object is available in res
:
app/* --> 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/* --> 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/* --> 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 //Route controllerapp/* --> 400 { status: false, error: { code: 2004, message: "'No such file or directory" }}*/ // You can customize the response error.data with:exceptionPool
Error response options
Responses object can contain more info about errors:
//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 = 'error' //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.jsconst router = express router // errorHandler is not necesary, main app will manage itmoduleexports = router // main.jsconst authRouter = const app = app expressDeliver /* 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 = // or const ResponseData = require('express-deliver').ResponseData app/* --> 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):
{ return '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:
// 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:
{ //This tells expressDeliver to ignore coroutine result res} /* --> 200 <html><head>...*/
Empty return
{ //Nothing here}//or{ 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 :
{ res return foo:20} /* --> 200 my previously sent data*/