Edit Page

Actions and controllers

Overview

#

Actions 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, and orchestrate the bulk of your project’s business logic: you can use actions to serve web pages, handle form submissions, manage 3rd party API requests, and everything in between.

Actions are bound to routes in your application. When a user agent requests a particular URL, the action bound to that route performs the business logic within and sends back 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 navigates to the /hello URL on your app's server, the page will display the message: “Hi there!”.

Defining your action

#

Actions are defined in the api/controllers/ folder and subfolders (we’ll talk more about controllers in a bit). In order for Sails to recognize a file as an action, the filename must be kebab-cased (containing only lowercase letters, numbers and dashes). When referencing an action in Sails (in most cases, when binding it to a route), use its path relative to api/controllers, without any file extension. For example, to bind a route to an action located at api/controllers/user/find.js, you would point its URL to user/find.

File extensions for actions
#

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. An action can have any file extension that isn't .md (Markdown) and .txt (text).

Creating an action

#

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

actions2
#

Since the release of Sails v1.0, we recommend writing your actions in the more modern "actions2" syntax, which works much the same way as Sails helpers. By defining your actions in this way, they are essentially self-documenting and self-validating.

Using actions2 provides several advantages:

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.

Here's an example of 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
    };
  }
};

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

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
#

If you're working with an existing codebase or an app that was upgraded from v0.12, you may be more used to the classic action format. Classic actions are declared as functions with req and res arguments. When a client requests a route bound to this type of action, the function runs using the incoming request object as the first argument (req), and the outgoing response object as the second argument (res).

Here's a sample action that looks up a user by ID, then 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

#

For simpler projects and prototypes, often 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
#

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

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

Using standalone actions has several advantages over controller files:

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

Next up: Views

Is something missing?

If you notice something we've missed or could be improved on, please follow this link and submit a pull request to the sails repo. Once we merge it, the changes will be reflected on the website the next time it is deployed.

Concepts