Service objects in Node.js

In MVC frameworks it’s usual that controllers contain a lot of code and are the hardest part to understand; in consequence - to debug and refactor. Why? One of the answers would be: controllers don’t respect Single Responsibility Principle. Their job starts when they receive an HTTP request, then they do at least a few of following:

  • parse the request
  • validate the request
  • communicate with the third parties
  • communicate with the database
  • do calculations
  • build the response

That’s quite a lot. No surprise that without proper separation, they can consist of thousands of lines of code. There is a number of patterns how to unload controllers, and I find service objects being one of the most powerful tool for that.

What are service objects?

Service object is an instance of class that encapsulates a single, non-trivial, business action within your project. It’s easier to understand their nature when explained the naming convention for them. The name should answer the question - what they do? This should be a verb in imperative form. This way they appear as kind of procedures in your system:

  • RegisterUser
  • CreateOrder
  • CalculateSalary
  • GenerateDiagram
  • ...

Those classes should have one public method - usually named call, perform, invoke, or similar. This way it’s obvious how to use them, and what they are for:

new RegisterUser().call(email, password)  

What’s important?

In the definition:

class that encapsulates a single, non-trivial, business action

...is one term worth explaining further: business. The service objects should be separated from technical details. In particular, in the web development, it means:

  • the HTTP layer - request, response, cookies, etc. The request should be parsed for the service object before it’s called, and similarly, the response should be built after the service object finished.

  • database, third parties - those should be handled for example via adapters.

Only business, core logic in the service object. Ideally, service objects should be readable by non-technical people.

It’s OK to throw exceptions in a service object - they are always called with an object that should be familiar with their API. But again, any exceptions thrown inside a service object should not be HTTP-related. Instead, a service object should throw domain-related exception that should be caught in the controller, and there "translated" to HTTP language.

Benefits of service objects

  • The code is much easier to reason about. There is no HTTP inside the service object, so we can only focus on business logic. Let the controllers do the HTTP-related work. That’s a nice separation of concerns, and in practice - a really big deal.

  • Since the service object is not aware of the HTTP, it can be called not only by the controllers, but also by:

    • tests
    • other service objects
    • external scripts
  • Better indication what the app actually does. You can simply scan the names of services to get the rough idea what kind of project you meet.

  • Easy to test. You don’t need to worry about the HTTP in your tests. Test only business output for given business input.

  • They can even help you in the testing. You can call the service objects to setup the state of the environment before actual assertions. For example, this might be the flow how to check if user can see their orders:

    • call RegisterUser service
    • call LoginUser service
    • call CreateOrder service
    • call ShowOrders service
    • assert

Example

Here is the example of the service object. It’s called FetchPage, and it returns the data of Facebook page for given id. It not only fetches the data via Facebook API, but also processes them in a way convenient to our controller.

var facebookAdapter = require('adapters/facebook/facebookAdapter');

module.exports = function FetchPage() {  
 var that = this;
 this.resolve = null;
 this.reject = null;
 this.response = null;

 this.run = function (pageId) {
   return new Promise(
     function (resolve, reject) {
       that.resolve = resolve;
       that.reject = reject;
       fetchPageInfo(pageId).then(onSuccess, onFail);
     }
   );
 };

 function fetchPageInfo(pageId) {
   var pathname = pageId;
   var options = {
     fields: 'name,about,link,location,posts.limit(5).fields(message,type)',
   };

   return facebookAdapter.fetch(pathname, options);
 }

 function onSuccess(rawResponse) {
   that.response = rawResponse;
   validateResponse();
   that.resolve(parseResponse());
 }

 function onFail(error) {
   that.reject(error);
 }

 function validateResponse() {
   if (!that.response.name)
     that.reject('Non-page object provided');
   else if (!that.response.location || !that.response.location.city)
     that.reject('Non-page object provided');
   else if (!that.response.posts)
     that.reject('Non-page object provided');

 }

 function parseResponse() {
   return {
     name: that.response.name,
     city: that.response.location.city,
     video_posts: that.response.posts.data.filter(isVideoType),
   };
 }

 function isVideoType(post) {
   return (post.type === 'video');
 }
};

Notice how this service is separated from the HTTP layer. The input for call method is pageId, just a string. It doesn’t matter if it came from the querystring, post parameters, another service, or your tests. The name is fetched always in the same manner.

Similarly, all the details about facebook communication are hidden behind facebookAdapter. Adapters are another pattern worth explaining further. Maybe next time :).

This service objects works on promises, in the spirit of Node. Those can be tricky though, how can we use such service? For example like this:

app.get('/facebook/page/:id', function (req, res) {  
 var pageId = req.params.id;
 new FetchPage().run(pageId)
   .then(function (page) { res.status(200).send(page); })
   .catch(
     function (error) {
       if (error.startsWith('(#803)')) {
         res.status(404).send({ error: 'Page not found' });
       }
       res.status(422).send({ error: 'Non-page id provided' });
     }
   );
});

Conclusion

The concept of service objects is widely known and used in the Rails community. At the same time, it’s surprisingly little known in the Node community (or maybe just little spoken?). It’s one of the easiest, yet powerful way to separate business logic from technical details.

As mentioned above, service objects are easy to test. It feels like this pattern was kind of designed for testability, which of course brings a huge value. In the next episode I will show you in detail and by example how to test service objects. Stay tuned!