Blog of Raivo Laanemets

Software development and personal stories.

Express API responses with Promises

On 2015-11-28

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).