Express API responses with Promises
JSON APIs are very common in web applications. I have needed an API for all of my web application projects, even if not for external systems, then at least for some parts of the frontend UI for serving XHR requests. I usually write the server-side code using Promises and I have found that a simplified JSend specification for API responses is a nice solution which leads to short and maintainable code. I'm using the Express framework in my Node.js applications and describe here how to use it with the technique.
JSend specification
JSend specification uses HTTP error codes only for transport errors, not for application errors. This makes a lot of sense as it is not always easy to map all application errors to HTTP error codes. There is only a limited set of HTTP status codes.
JSend divides responses (with the usual status code 200) into 3 categories by its own status flag/property:
- success - successful request;
- failure - issue with the submitted data (client error?);
- error - error processing the request.
As Promises resolve to one of the two states, this can be simplified to only two statuses:
- success - succeful request, promise resolves to a fulfilled state;
- error - error processing the request, promise resolves to a rejected state.
In practice, I have not seen many use cases where the failure
state was useful. This state could be used for validation errors but I
have handled these through the error
state by specifying validation messages as additional data for the response. Having only two
states also simplifies the client code.
Example of a success response:
{
"status":"success",
"data":{ ... }
}
Example of an error response:
{
"status":"error",
"message":"Error occurred"
}
Enablement middleware
We create an helper method api
on the res
object to make it easier to form API responses. Besides sending the response,
the method should log down errors, including their stacktrace. To attach the method to incoming res
objects, we create an Express
middleware.
The middleware (api.js
) looks like this:
module.exports = function () {
return function (req, res, next) {
res.api = function (promise) {
promise.then(
function (data) {
res.send({ status: "success", data: data });
},
function (err) {
// Log the error here
// ...
var message = err.message ? err.message : "Unknown error";
res.send({ status: "error", message: err.message });
}
);
};
next();
};
};
The attached api
method takes a promise as an argument and forms the response based on the state that the promise resolves into. If it
resolves to a fulfilled state, a success response is sent (if the value of data
in the callback is undefined, the data property will
not appear in the response). In a case of a rejected state, the error should be logged down (I usually log directly to stderr - monitored by a
real-time logging service - and add the stacktrace). The error message is also sent back with the API response.
In some cases you don't get an Error instance but something
else (with a missing message
property). I have seen this
very rarely but it can happen. This is handled by checking the existence of the message
property.
Some people would argue that responding with the technical error message could be helpful for hackers to break into the application. This is true. However, I do not see this much of an issue in practice if errors are monitored and fixed as soon as possible. Generic "Application error" or no message at all could be sent back as well. This custom middleware is easy to tailor for specific application or security requirements.
Using the middleware
The usage can be demonstrated with the following example application. The application is the minimal Express application with a single route
/api/something
to respond with a JSON response. A response will be a success or an error with a 50% probability. The
api
method on the res
object makes the route handler code really simple:
app.get("/api/something", function (req, res) {
res.api(returnsPromise());
});
The returnsPromise
function could be something much more complex, like returning response from database. The good thing about it is
automatic propagation of data and errors through the Promise chains.
var express = require("express");
var app = express();
// Add the middleware (in jsend.js file).
app.use(require("./api")());
app.get("/api/something", function (req, res) {
res.api(returnsPromise());
});
var server = app.listen(3000, function () {
console.log("Example app started listening");
});
function returnsPromise() {
return new Promise(function (resolve, reject) {
if (Math.random() < 0.5) {
resolve("Hello world!");
} else {
reject(new Error("Error occurred"));
}
});
}
After starting the application we can run requests against it.
Success response:
$ curl http://localhost:3000/api/something
{"status":"success","data":"Hello world!"}
Error response:
$ curl http://localhost:3000/api/something
{"status":"error","message":"Error occurred"}
The example application and the middleware were tested with Express 4.13. There are various packaged JSend/API solutions on NPM but I have not found any that satisfied all my requirements (support Promises, custom error logging).