Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
207 lines (142 sloc) 12.2 KB

Actions and controllers

Overview

Actions are the principal objects in your Sails application that are responsible for responding to requests from a web browser, mobile application or any other system capable of communicating with a server. They often act as a middleman between your models and views. With rare exceptions, the actions will orchestrate the bulk of your project’s business logic.

Actions are bound to routes in your application, so that when a user agent requests a particular URL, the bound action is executed to perform some business logic and send a response. For example, the GET /hello route in your application could be bound to an action like:

async function (req, res) {
  return res.send('Hi there!');
}

Any time a web browser is pointed to the /hello URL on your app's server, the page will display the message: “Hi there!”.

Where are actions defined?

Actions are defined in the api/controllers/ folder and subfolders (we’ll talk more about controllers in a bit). In order for a file to be recognized as an action, it must be kebab-cased (containing only lowercase letters, numbers and dashes). When referring to an action in Sails (for example, when binding it to a route), use its path relative to api/controllers, without any file extension. For example, the api/controllers/user/find.js file represents an action with the identity user/find.

File extensions for actions

An action can have any file extension besides .md (Markdown) and .txt (text). By default, Sails only knows how to interpret .js files, but you can customize your app to use things like CoffeeScript or TypeScript as well.

What does an action file look like?

Action files can use one of two formats: actions2 (recommended) or classic.

actions2

Since the release of Sails v1.0, the recommended approach to create an action is by writing it in the more modern ("actions2") syntax. In much the same way that Sails helpers work, by defining your action with a declarative definition ("machine"), it is essentially self-documenting and self-validating. Here's the actions2 format:

module.exports = {

   friendlyName: 'Welcome user',

   description: 'Look up the specified user and welcome them, or redirect to a signup page if no user was found.',

   inputs: {
      userId: {
        description: 'The ID of the user to look up.',
        // By declaring a numeric example, Sails will automatically respond with `res.badRequest`
        // if the `userId` parameter is not a number.
        type: 'number',
        // By making the `userId` parameter required, Sails will automatically respond with
        // `res.badRequest` if it's left out.
        required: true
      }
   },

   exits: {
      success: {
        responseType: 'view',
        viewTemplatePath: 'pages/welcome'
      },
      notFound: {
        description: 'No user with the specified ID was found in the database.',
        responseType: 'notFound'
      }
   },

   fn: async function ({userId}) {

      // Look up the user whose ID was specified in the request.
      // Note that we don't have to validate that `userId` is a number;
      // the machine runner does this for us and returns `badRequest`
      // if validation fails.
      var user = await User.findOne({ id: userId });

      // If no user was found, respond "notFound" (like calling `res.notFound()`)
      if (!user) { throw 'notFound'; }

      // Display a personalized welcome view.
      return {
        name: user.name
      };
   }
};

You can use sails generate action to quickly create an actions2 action.

Sails uses the machine-as-action module to automatically create route-handling functions out of machines like the example above. See the machine-as-action docs for more information.

Note that machine-as-action provides actions with access to the request object as this.req.

Using classic req, res functions for your actions is technically less typing. However, using actions2 provides several advantages:

  • the code you write is not directly dependent on req and res, making it easier to re-use or abstract into a helper
  • you guarantee that you’ll be able to quickly determine the names and types of the request parameters the action expects, and you'll know that they will be automatically validated before the action is run
  • you’ll be able to see all of the possible outcomes from running the action without having to dissect the code

In a nutshell, your code will be standardized in a way that makes it easier to re-use and modify later. And since you'll declare the action's parameters ahead of time, you'll be much less likely to expose edge cases and security holes.

Exit signals

In an action, helper, or script, throwing anything will trigger the error exit by default. If you want to trigger any other exit, you can do so by throwing a "special exit signal". This will either be a string (the name of the exit), or an object with the name of the exit as the key and the output data as the value. For example, instead of the usual syntax:

return exits.hasConflictingCourses();

You could use the shorthand:

throw 'hasConflictingCourses';

Or, to include output data:

throw { hasConflictingCourses: ['CS 301', 'M 402'] };

Aside from being an easy-to-read shorthand, exit signals are especially useful if you're inside of a for loop, forEach, etc., but still want to exit through a particular exit.

Classic actions

The traditional way of getting started creating a Sails action is to declare it as a function. When a client requests a route that is bound to that action, the function will be called using the incoming request object as the first argument (typically named req), and the outgoing response object as the second argument (typically named res). Here's a sample action function that looks up a user by ID, and either displays a "welcome" view or redirects to a signup page if the user can't be found:

module.exports = async function welcomeUser (req, res) {

  // Get the `userId` parameter from the request.
  // This could have been set on the querystring, in
  // the request body, or as part of the URL used to
  // make the request.
  var userId = req.param('userId');

   // If no `userId` was specified, or it wasn't a number, return an error.
  if (!_.isNumeric(userId)) {
    return res.badRequest(new Error('No user ID specified!'));
  }

  // Look up the user whose ID was specified in the request.
  var user = await User.findOne({ id: userId });

  // If no user was found, redirect to signup.
  if (!user) {
    return res.redirect('/signup' );
  }

  // Display the welcome view, setting the view variable
  // named "name" to the value of the user's name.
  return res.view('welcome', {name: user.name});

}

You can use sails generate action with --no-actions2 to quickly create a classic action.

Controllers

The quickest way to get started writing Sails apps is to organize your actions into controller files. A controller file is a PascalCased file whose name must end in Controller, containing a dictionary of actions. For example, a "User Controller" could be created at api/controllers/UserController.js file containing:

module.exports = {
  login: function (req, res) { ... },
  logout: function (req, res) { ... },
  signup: function (req, res) { ... },
};

You can use sails generate controller to quickly create a controller file.

File extensions for controllers

A controller can have any file extension besides .md (Markdown) and .txt (text). By default, Sails only knows how to interpret .js files, but you can customize your app to use things like CoffeeScript or TypeScript as well.

Standalone actions

For larger, more mature apps, standalone actions may be a better approach than controller files. In this scheme, rather than having multiple actions living in a single file, each action is in its own file in an appropriate subfolder of api/controllers. For example, the following file structure would be equivalent to the UserController.js file:

api/
 controllers/
  user/
   login.js
   logout.js
   signup.js

where each of the three JavaScript files exports a req, res function or an actions2 definition.

Using standalone actions has several advantages over controller files:

  • it's easier to keep track of the actions that your app contains by looking at the files contained in a folder than by scanning through the code in a controller file
  • each action file is small and easy to maintain, whereas controller files tend to grow as your app grows
  • routing to standalone actions in nested subfolders is more intuitive than in nested controller files (foo/bar/baz.js vs. foo/BarController.baz)
  • blueprint index routes apply to top-level standalone actions, so you can create an api/controllers/index.js file and have it automatically bound to your app’s / route (as opposed to having to create an arbitrary controller file to hold the root action)

Keeping it lean

In the tradition of most MVC frameworks, mature Sails apps usually have "thin" controllers—that is, your action code ends up lean because reusable code has been moved into helpers or occasionally even extracted into separate node modules. This approach can definitely make your app easier to maintain as it grows in complexity.

But at the same time, extrapolating code into reusable helpers too early can cause maintenance issues that waste time and productivity. The right answer lies somewhere in the middle.

Sails recommends this general rule of thumb: wait until you're about to use the same piece of code for the third time before you extrapolate it into a separate helper. But, as with any dogma, use your judgement! If the code in question is very long or complex, then it might make sense to pull it out into a helper much sooner. Conversely, if you know what you're building is a quick, throwaway prototype, you might just copy and paste the code to save time.

Whether you're developing for passion or profit, at the end of the day, the goal is to make the best possible use of your time as an engineer. Some days that means getting more code written, and other days it means looking out for the long-term maintainability of the project. If you're not sure which of these goals is more important at your current stage of development, you might take a step back and give it some thought (better yet, have a chat with the rest of your team or other folks building apps on Node.js/Sails).

You can’t perform that action at this time.