Greetings, Sailors! On our previous voyage you learned how to associate models to other models in Sails.js. Using explicit associations works well for integrating common relationships into your code, but not all operations fit into just one model instance. This installment will introduce you to Sails controllers, which you’ll use to manage more complex operations in your Sails applications.

Customizing model operations

Until now we’ve been sailing along pretty smoothly, using Sails’s default routes to access or modify model instances. These defaults, part of the Sails Blueprint API, take care of the basic create, read, update, and delete functionality we expect from web or mobile applications. But any developer working on a production HTTP API will tell you that simple CRUD only goes so far. Even if you are starting from there, you need to be able to customize how basic routes map to controllers.

The Blueprint API will get you rolling, but eventually you’ll need something more powerful, flexible, and customizable (or all three) to build the apps your client and users want. For most development cycles, you’ll use the Blueprint routes for prototyping, then replace this scaffolding with customized routes and associated controllers.

In addition to the CRUD routes, a few other controller-route combinations come pre-defined in a stock Sails.js installation. For the most part, though, you’ll want to create your own mappings to get the behavior that you need.

Mapping complex queries

In the previous article you extended the blog API we started with to a more expansive CMS backend. While you currently envision this app powering a weblog, it could be put to other uses. The RESTful API is accessible to virtually any front-end app that wants to grab and display blog entries or RSS feeds, and it allows for search queries. Extending the API left you with several new model types, namely Author, Entry, and Comment. Every Entry holds an Author and Comments, and these types link back to their reference Entry, as well.

With the switch to a CMS, it’s increasingly clear that you’ll want to be able to set up operations that Sails’s default routes don’t support. Say you wanted to be able to make complex queries (such as for an entry written by a particular author, after a certain date, and organized by a given criteria), or obtain all entries corresponding to a particular tag. You might need to provide entries not in their native JSON format, but in an XML format compatible with RSS or Atom. On the input side, you might want to add an import feature, enabling the CMS to grab and store the entirety of an existing blog via its RSS or Atom feed.

Really, there are a thousand things you could do, and many of them are not supported by Sails defaults.

What controllers do

In a traditional MVC pattern, controllers define the interaction between models and their views. When a model’s data changes, a controller will ensure that each of the views attached to that model updates accordingly. Controllers also receive a notification whenever a user does something inside a view. If that action necessitates a change to a model, the controller will issue notifications to all the affected views. From an architectural perspective, models, controllers, and views are all defined on the server. Views are usually some form of HTML template (ideally with minimal code in it); models are domain classes or objects; and controllers are the blocks of code behind the routes.

In an HTTP API, the relationship between the three application components is similar but not identical to MVC. Unlike an MVC architecture, the model, view, and controller of an HTTP API aren’t all held on the same server. Views in particular will often be off the server, either as a single-page web application or a mobile app.

Architecturally the HTTP API holds together more or less the same way that an MVC application would:

  • Views display the data received via the HTTP API.
  • Models are the artifacts exchanged across the wire. (An HTTP API uses a type of model designed for easy transport, sometimes called a ViewModel.)
  • Controllers are the verbs of the API; they exist to “do” something rather than just “be” something.

In an HTTP API, when a view hits the controller endpoint with an HTTP request, that request is tied directly to a controller for execution. The controller takes the data that was passed in through either the URL or the body of the request, performs some action against one or more existing models (or creates new ones), and generates the response back to the view for updating. The controller’s response will usually be a combination of the HTTP status code and a JSON body, as defined by the API developer.

Okay, that’s enough theory. Let’s control.

Create a controller

The easiest way to get started with Sails controllers is to create one that returns static data. In this case, you’ll create a simple controller that returns a user-friendly version of the CMS API. This will be a quick-and-easy API that business clients can use to test whether the server is up and running. Clients will also be able to see at a glance any recent changes, such as upgrades to the API, and adjust their web or mobile front-end accordingly.

There are two ways to create a new controller in Sails: you can use the sails command to generate the scaffolding, or you can simply have the files generated for you. In the latter case the generator uses the naming system defined by the Sails convention rules to identify and place files, which are mostly empty. For this first controller, you’ll use the generator:


~$ sails generate controller System
info: Created a new controller ("System") at api/controllers/SystemController.js!

Controllers live in your Sails project’s api/controllers directory. By convention they have the controller suffix. If you append additional descriptors after the controller name, Sails will assume that these are operations (methods) to be used on the controller. You can save a few steps by specifying operations up front; otherwise the default generator generates an entirely empty controller, as shown here:


  /*
    SystemController
   
    @description :: Server‑side logic for managing the System
    @help        :: See http://sailsjs.org/#!/documentation/concepts/Controllers
   /

  module.exports = {
    
  };

Running a slightly different command —sails generate controller System version—gives you more to work with:


  /
    SystemController
   
    @description :: Server‑side logic for managing Systems
    @help        :: See http://sailsjs.org/#!/documentation/concepts/Controllers
   */

  module.exports = {

    /
      SystemController.version()
     /
    version: function (req, res) {
      return res.json({
        todo: 'version() is not implemented yet!'
      });
    }
  };

Adding commands lays out scaffolded method endpoints, though as you can see these currently don’t do much but return helpful error messages. Using the generator to create the scaffolded controller can be helpful at first. Over time, many developers end up just doing a File|New in their favorite text editor. If you decide to do that, you’ll need to name the file and endpoint correctly (for instance, FooController in foocontroller.js) and write the exported functions by hand. Neither approach is right or wrong, so use what works best for you.

Implementing the simple first controller is child’s play:


module.exports = {

  /*
    SystemController.version()
   */
  version: function (req, res) {
    return res.json({
      version: '0.1'
    });
  }
};

Bind and invoke the controller

Next you’ll want to bind, then invoke your controller. Binding maps the controller to a route; invoking hits it with the appropriate HTTP request. The default route for this controller will be /System/version, based on the controller’s file name and the name of the method being invoked.

Personally, I’d rather not have “system” in the URL, and would prefer to use “/version.” Sails allows you to bind the controller’s invocation to any routing choice, so let’s set it up to bind to /version.

You can set up a controller route by creating an entry in the Sails routing table, stored at config/routes.js. This default file is generated whenever you create a Sails app. Once you strip away all the comments, this file is pretty empty:


  module.exports.routes = {

    /*
                                                                              
     Make the view located at views/homepage.ejs (or views/homepage.jade, 
     etc. depending on your default view engine) your home page.              
                                                                              
     (Alternatively, remove this and add an index.html file in your         
     assets directory)                                                      
                                                                              
    */

    '/': {
      view: 'homepage'
    }

  };

In essence, config/routes.js holds a collection of routes and targets. Routes are relative URLs, and targets are what you want Sails to invoke. The default route is the / URL pattern, which brings up Sails’s default homepage. If you wanted to, you could replace this default with a stock HTML page. In that case the HTML would live in your assets directory, right next to the api directory we’ve been working in all along.

Upgrading the CMS’s HTML might be a fun exercise for a single-page application framework like React.js, but the goal here is to add a /version route and have it target the SystemController.version method. For that you just need to add one additional line to the JSON object imported by config.js:


module.exports.routes = {

  'get /version': 'SystemController.version'

};

Once you’ve saved this route in the config.js file, issuing an HTTP GET to /version will send back the same JSON result you set up earlier.

Controllers in the Blueprint API

When you create a model, Sails’s tooling automatically creates a controller for it. So, even though you haven’t set out to create them, each of your models already has a controller—AuthorController, EntryController, and so on.

To get more familiar with Sails controllers and routes, let’s say that you want AuthorController to hold a method that returns the aggregate list of bios for all authors in your CMS. The implementation is pretty straightforward—just get all the Authors, extract their bios, and hand back the list:


module.exports = {

  bios: function(req, res) {
    Author.find({})
      .then(function (authors) {
        console.log("authors = ",authors);
        var bs = [];
        authors.forEach(function (author) {
          bs.push({
            name: author.fullName,
            bio: author.bio
          });
        });
        res.json(bs);
      })
      .catch(function (err) {
        console.log(err);
        res.status(500)
          .json({ error: err });
      });
  }

};

If you’ve used the default /author/bios route you won’t even need a special entry in your routes.js. In this case the default works fine (though you know how to change it if you disagree), so we’ll just leave it at that for now.

Testing state

You might find a seed controller useful for certain kinds of testing. This type of controller initializes the database to a known state, like so:


module.exports = {
    run: function(req, res) {
        Author.create({
            fullName: "Fred Flintstone",
            bio: "Lives in Bedrock, blogs in cyberspace",
            username: "fredf",
            email: "fred@flintstone.com"
        }).exec(function (err, author) {
            Entry.create({
                title: "Hello",
                body: "Yabba dabba doo!",
                author: author
            }).exec(function (err, created) {
                Entry.create({
                    title: "Quit",
                    body: "Mr Slate is a jerk",
                    author: author.id
                }).exec(function (err, created) {
                    return res.send("Database seeded");
                });
            });
        });
    }
};

In this case the known state is bound to the controller’s default route, /seed/run. Alternatively, you can set up different seed methods for different testing and/or development scenarios. Use a curl command to set the database to a particular state for the route in question. Just make sure these routes are disabled or removed in your production code.

Managing controller input

So far you’ve been working with really simple controllers that take no input. Often, however, a controller will need to take input from the caller. There are three types of controller input:

  1. Form parameters sent in the request body. This is the traditional mechanism for accepting input over the web.
  2. Input data sent through a JSON object as part of the request body. This is the same idea as form parameters but the content type sent is application/json rather than form/multipart-form-data. Input data is typically easier for clients to generate and is much easier for the server to consume.
  3. Input specified through parameters and sent via placeholders in the URL route. When requesting a particular author’s entries, you’ll often want the author identifier to be passed as part of the URL itself. An example would be /author/1/entries, where “1” is the author’s unique identifier. In this way, you preserve the appearance that blog entries are part of the author resource, even if the entries aren’t physically stored with the author. (This is true for the example app, where Entry objects are stored in a separate collection or table from Author objects.)

Form parameters are the domain of the traditional Express-style request.getParam() calls, and have been well-documented elsewhere. It’s also not common for HTTP APIs to use form parameters, so we’ll discard that approach for now. The second approach is a good fit for the CMS app.

Obtaining input

Capturing values sent through a JSON object is usually is as simple as a request.body.field. Or, if the input is an array at the topmost level of the JSON, you could use a request.body[idx].field.

First off, you’ll create an endpoint that will return an RSS XML feed for all the Entry objects in the CMS database. (In a real-world system, this number would need to be constrained to support a pagination scheme like returning the top 20 of the most recent entries, but we’re keeping it simple for now.) Name your controller (I’ve chosen FeedController below), and put an RSS method on it so that the default route (/feed/rss) makes sense:


var generateRSS = function(entries) {
  var rss = 
    '<rss version="2.0">' +
    '<channel>' + 
    '<title>SailsBlog</title>';

  // Items
  entries.forEach(function (entry) {
    rss += '<item>' +
      '<title>' + entry.title + '</title>' +
      '<description>' + entry.body + '</description>' +
      '</item>';
  });

  // Closing
  rss += '</channel>' +
    '</rss>';
  return rss;
}

module.exports = {

  rss: function (req, res) {
    Entry.find({})
      .then(function (entries) {
        var rss = generateRSS(entries);
        res.type("rss");
        res.status(200);
        res.send(rss);
      })
      .catch(function (err) {
        console.log(err);
        res.status(500)
          .json({ error: err });
      });
  
    return res;
  }
};

Now, this is a nice little feed, but what if you wanted to restrict the feed to one or more particular authors? In that case, you would set up a new route (/author/{id}/rss). The new route would take the identifier passed in the URL, then use that to constrain the query to find only entries by the given author. The remainder of the RSS method would be essentially the same.

Let’s see how that works in code. First, FeedController gets a per-author RSS method, just like before:


var generateRss = // as before

module.exports = {

  rss: // as before
  authorRss: function(req, res) {
    Entry.find({ 'author' : req.param("authorID") })
      .then(function (entries) {
        var rss = generateRSS(entries);
        res.type("rss");
        res.status(200);
        res.send(rss);
      })
      .catch(function (err) {
        console.log(err);
        res.status(500)
          .json({ error: err });
      });
  
    return res;
  }
};

What’s different above is the query, which is now constrained to find all the Entry objects that have an author ID. Note the instruction by the parameter authorID coming in on the HTTP request. The mapping of authorID (in this case to the third part of the incoming URL pattern) is specified in the routes.js file, as shown:


var generateRss = // as before

module.exports = {

  rss: // as before
  authorRss: function(req, res) {
    Entry.find({ 'author' : req.param("authorID") })
      .then(function (entries) {
        var rss = generateRSS(entries);
        res.type("rss");
        res.status(200);
        res.send(rss);
      })
      .catch(function (err) {
        console.log(err);
        res.status(500)
          .json({ error: err });
      });
  
    return res;
  }
};

The parameter specified in the routes file is case sensitive, so make sure you maintain consistent naming conventions. Case difference is a frequent but subtle source of bugs in application code.

Conclusion

Models deal with the pure data-related definitions, whereas controllers deal with everything in the application logic that isn’t data specific. Unless you need particular logic or constraints, there’s no reason to replace the standard REST controllers found in the Blueprint API. Even if you need particular logic or constraints you can frequently use an existing Blueprint route as a starting point.

If you’ve followed this series from the beginning, you now have a good feel for Sails.js. You can fire it up on IBM Cloud, define some models, access and update your models, insert/update/remove and query for them, and create endpoints that aren’t specific to the models being passed around. In short, you’ve got all the tools you need to get started with Sails.js in a non-trivial HTTP API project.

Obviously there’s more to explore. At some point you’ll likely need to know how to plug in your own ORM/ODM (versus using the default of Waterline, introduced in Part 2) or dive into the deep end of configuration options for the Sails engine. You might also want to pair Sails with Ember.js for a functional and workable SANE stack. But for now, you’ve learned the ropes, and that means it’s time for one last bon voyage!