Sailing With Sails.js: An MVC-style Framework For Node.js

About The Author

Slava Fomin II is an entrepreneur and a passionate Web-developer. As a founder of the Better Solutions studio, he tries to make Web a better place by combining … More about Slava ↬

Email Newsletter

Weekly tips on front-end & UX.
Trusted by 200,000+ folks.

In this introductory article, Slava Fomin II will show you the most important parts of the Sails framework and give you some specific examples to get you going. Of course, if you want to use it in your daily work, you will have to spend some time mastering it and taking it to the next level. The good news is that Sails comes with pretty solid documentation and an active community. The creator of Sales even answers questions on StackOverflow personally. He’ll neither confirm nor deny that Sails is being developed by a giant smart octopus, but he will do his best to guide you from the humble ensign to being the confident captain of your own ship! You will not be alone.

I had been doing server-side programming with Symfony 2 and PHP for at least three years before I started to see some productivity problems with it. Don’t get me wrong, I like Symfony quite a lot: It’s a mature, elegant and professional framework. But I’ve realized that too much of my precious time is spent not on the business logic of the application itself, but on supporting the architecture of the framework.

I don’t think I’ll surprise anyone by saying that we live in a fast-paced world. The whole startup movement is a constant reminder to us that, in order to achieve success, we need to be able to test our ideas as quickly as possible.

The faster we can iterate on our ideas, the faster we can reach customers with our solutions, and the better our chances of getting a product-market fit before our competitors do or before we exceed our limited budget. And in order to do so, we need instruments suitable to this type of work.

Sailing With Sails.js

If you’re developing a complex application with three hundred pages of documentation for some big corporate client and you know most of its details from the start, then Symfony 2 or some enterprise Java framework would probably be the best tool for the job. However, if you are a startup developer or you just want to test some of your ideas quickly without compromising the overall quality of the application, then Sails (or Sails.js) is a very interesting candidate to consider.

I’ll neither confirm nor deny that Sails is being developed by a giant smart octopus, but I will do my best to guide you from the humble ensign to being the confident captain of your own ship!

Introduction

Sails is a comprehensive MVC-style framework for Node.js specifically designed for rapid development of server-side applications in JavaScript. It’s robust service-oriented architecture provides different types of components you can use to neatly organize code and separate responsibilities. And if you’re disciplined, then developing an enterprise-level application with it is even possible.

Written in JavaScript, Sails gives you the additional benefit of being able to share your code between the server and client. This could be very helpful, for example, for implementing data validation where you need to have the same validation rules both in the client and on the server. Also, with Sails you need to master only one programming language, instead of several.

One major concept of the framework is that it wraps a stack of loosely coupled components. Almost every aspect of the system is customizable: You can add, remove or replace most of the core components without compromising the framework’s overall stability. In other words, if you need to get a job done as quickly as possible, Sails will help you by providing robust built-in components with sensible defaults; however, if you’d like to create a fully customized solution, Sails will not stand in your way either. If you are already familiar with the philosophy behind the Node.js development community, then you will get what I mean; if not, then you will understand it during the course of this article.

Under the hood, Sails contains probably the most well-known web framework for Node.js, Express. Express is a very simple, basic framework. It provides the mere bones for your web development needs. To implement a serious web app with it, you will need to find and integrate a bunch of third-party components yourself. Also, Express doesn’t really care about the structure of code or a project’s file system, so you will need to manage that yourself and come up with a sensible structure. That’s where Sails comes to the rescue. Built on top of Express’ robust design, it provides all required components out of the box and gives developer a well-thought-out organization for their code and project files. With Sails, you will be able to start development with the built-in and documented tools.

I believe the best way to understand something is to get ahold of it and explore it firsthand. So, enough talking. Let’s grab the code and create our first local project!

Getting Started

I’ll start with a clean slate. Let’s begin by installing all of the requirements and the latest version of Sails itself.

I’m using Ubuntu Linux, so all commands will be presented for this OS. Please adjust them according to your working environment.

Install Node.js

To install the latest version of Node.js on your Ubuntu machine from NodeSource Node.js Binary Distributions, just run these three commands:


# Make Sure cURL Is Available In The System
sudo apt-get install -y curl

# Adding NodeSource Repository To The System Via Provided Script
curl -sL https://deb.nodesource.com/setup_dev | sudo bash -

# Actually installing The Node.js From The NodeSource Repository
sudo apt-get install -y nodejs

You can confirm that Node.js has been successfully installed by using this command:

node --version

It should output something like v0.12.4.

Note: If you’re not using Ubuntu, then please see Joyent’s instructions on installing Node.js on different platforms.

Install Sails

The following command will install Sails globally:

sudo npm -g install sails

You can test whether the framework was installed with this command:

sails --version

It should output the number of the latest stable version of Sails.

Create A Project

Let’s create the test project that we will be experimenting with:


sails new sails-introduction
cd ./sails-introduction

Start A Project

The most interesting aspect of Node.js is that the application doesn’t require an external web server in order to operate. In the world of Node.js, the application and the web server are the same thing. When you run your Sails application, it binds to the given port and listens for HTTP requests. All requests are handled in the same OS process sequentially by your application. (In contrast, Apache will spawn multiple sub-processes or threads, and each request will have its own context space.)

So, how can your application serve multiple requests without those requests noticeably blocking each other? The key to this is a major feature of the Node.js: asynchronosity. All heavy operations, such as I/O and database access, are performed in a non-blocking asynchronous fashion. Every asynchronous method allows you to specify a callback function, which is activated as soon as the requested operation completes. The result of the operation (or error description) gets passed to your callback function. That way, your application can delegate all heavy work and continue with its own business, returning later to collect the results and continue where it left off.

Note: The more convenient and modern approach is to use promises instead of callback functions, but that is beyond the scope of this article. Please see Jake Archibald’s article for more insight into the topic.

Let’s start our project to see that everything is working fine. Just run the following:

sails lift

Sails will initialize the application, bind to the configured port and start to listen for HTTP requests.

Note: When your application is lifted, the terminal window will be in blocked state. You can press Control + C to terminate the application and return to the command prompt.

Now, you will be able to open the default application in your favorite browser by visiting https://localhost:1337/.

At this point, the default page should load correctly.

Diving Into Sails

Now, let’s dissect our project to understand what makes it tick!

Sails is an MVC framework, so starting from these components to see what glues them all together makes sense.

The entry point to our application is the app.js file, which lies at the root of the project. You could call it a front controller if you’d like; however, it wouldn’t make sense to edit its content. All it does is require top-level dependencies and give control to Sails itself. After that, all of the magic happens in the framework.

Routing Component

When Sails receives an HTTP request, it actually uses its router component to find the controller responsible for generating the response. Router matching can be controlled through a special configuration file located at config/routes.js. If you open this file now, you will see that it contains only a single entry:


module.exports.routes = {
  '/': {
    view: 'homepage'
  }
};

Note: The default project for Sails contains a lot of comments, which were introduced specifically to speed up project configurations and ease the learning curve. Feel free to remove them if you’d like. No code snippets in this article will contain any built-in comments, in order to preserve space and improve readability.

The left part of the expression, ’/’, is the URL pattern that tells Sails that the following configuration (the right part) should be used for an index page. The view property of the configuration contains the homepage value, which is the name of the view (the V in MVC).

Views Layer

Views are handled by a separate component of the framework. With the help of the “Consolidate” Node.js package, Sails supports at least 31 different templating languages. So, choose the language that is most suitable for you, your project and your team.

All templates are located in the views directory of your project. You will find there the aforementioned views/homepage.ejs template file that is used to render the home page, and you can play with it if you like.

Note: All templates are rendered dynamically on the server. You will not need to restart Sails in order to refresh any changed templates. All changes will be shown immediately upon the page being refreshed. Try it!

If you look at the homepage.ejs template, you will notice that it’s not complete. It’s missing basic HTML elements, such as the DOCTYPE, html, head body tags. This is on purpose. The most reusable parts of the template are extracted into a separate template file, views/layout.ejs. The name of the layout template is configured in the config/views.js file (look for the layout property). This really helps to keep things DRY. However, if you need to use another layout for some particular page, you can easily override the property dynamically in your controller.

Be advised that this layout configuration works only for the default EJS templating system and will not work with other languages. This is done for the purpose of legacy- and backwards-compatibility. Using the layout functionality provided by the templating language of your choice is recommended. For example, in Twig and Jinja2, you can use the extends expression to extend a parent template and overload required blocks.

Using Custom Views Engine

This section demonstrates how to change the views engine that is used to render templates in Sails. This should give you an idea of how easy some parts of Sails can be overridden and customized. I’m going to use the Twig/Jinja2 templating language, because of its flexibility and extensibility. I’ve been using it for at least three years now, and the language has never constrained me in any way. So, I highly recommend you try it.

Note: Twig and Jinja2 are a common family of templating languages with the same core functionality and features. However, each concrete implementation can have its own small differences and flavors. I will be using the Swig library during the course of this article. It provides a concrete implementation of the Twig and Jinja2 templating syntax for Node.js. Please see Swig’s official documentation for more details.

As I said earlier, Sails delegates view rendering to the Node.js package called “Consolidate.” This package actually consolidates about 30 different view engines. I will be using the Swig view engine, which implements support for the Twig and Jinja2 templating languages. To use it, I will need to complete a few simple steps:

  1. Define dependencies and install the Swig package: npm install --save swig.
  2. Change Sails’ configuration a bit by editing the config/views.js file. All you need to do is to set the engine property to swig.
  3. Rewrite all templates from EJS format to Twig and Jinja2. Don’t forget to change the extension to .swig!
  4. Reload the Sails server.

Note: In order to see the changes, you will need to reload the application by terminating the server and then lifting it again.

An answer on Stack Overflow gives some hints on how this can be automated.

The content for all of the changed files are provided below for your reference.

config/views.js:


module.exports.views = {
  engine: 'swig'
};

views/layout.swig:


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>{{ title|default('The Default Title') }}</title>
  </head>
  <body>
    {% block body %}{% endblock %}
  </body>
</html>

views/homepage.swig:


{% extends 'layout.swig' %}
{% set title = 'Homepage Title' %}
{% block body %}
  <h1>Homepage!</h1>
  <p>Welcome to the homepage!</p>
{% endblock %}

views/404.swig:


{% extends 'layout.swig' %}
{% set title = 'Page Not Found' %}
{% block body %}
  <h1>{{ title }}</h1>
{% endblock %}

The content for 403.swig and 500.swig is almost the same as for 404.swig presented above. I will leave it to you to fix the files yourself.

The Controller

OK, we’ve looked into the routes and views components, but where is the controller part of the MVC, you ask? Actually, the default Sails project is so simple that it doesn’t require any custom logic. If you open the api/controllers directory, you will see that it’s empty.

As you have guessed, Sails can even run without a controller; the routing configuration may specify the view directly, without the need of a controller. This could be a useful feature for static pages that don’t require any input from the user or any additional processing, as is the case with our home page right now. But let’s fix this shortcoming and introduce some business logic into our route.

Let’s create a new controller for our home page with the following command:

sails generate controller homepage

Sails will generate a file for you, api/controllers/HomepageController.js.

We can open this file and introduce a new action for our home page. I will call it index:


module.exports = {
  index: function (request, response) {
    return response.view('homepage', {
      currentDate: (new Date()).toString()
    });
  }
};

This simple action will just render our homepage view that we discussed earlier and pass an additional variable to it called currentDate, which will contain the textual presentation of the current date.

Note: The controller’s action is a simple JavaScript function that accepts two arguments: the special request and response objects. These objects correspond directly to the objects provided by the Express framework. Please look at Express’ documentation for the API details.

To make Sails actually use our controller, we need to slightly alter the routing configuration in the config/routes.js file:


module.exports.routes = {
  '/': 'HomepageController.index'
};

Here, we are telling the system to give control of the request to our HomepageController and, specifically, its index action. Now, the controller is responsible for handling the request and generating the response.

Also, don’t forget to add the following line to the views/homepage.swig:


<p>Current date is: {{ currentDate }}</p>

This will render the date string passed from the controller.

Now, reload the server and refresh the page. You should see the changes.

Shadow Routes For Actions

By default, Sails will generate implicit routes (also called shadow routes) for every controller’s action. The generated URL will look like /:controller/:action. In our case it will be https://localhost:1337/homepage/index. Although, this feature can be useful, sometimes it’s not desired (such as when you get two URLs for a home page, as in our case).

You can control this behavior by configuring the blueprints component, which can be done in two place. The first and most obvious place is the config/blueprints.js configuration file. You can disable action shadow routes for an entire application by setting the actions option to false:


module.exports.blueprints = {
  actions: false
};

However, to disable shadow routes for a single controller only, you would set it in the controller itself, api/controllers/HomepageController.js:


module.exports = {
  _config: {
    actions: false
  },
  index: function (request, response) {
    return response.view('homepage', {
      currentDate: (new Date()).toString()
    });
  }
};

The special _config option of the controller’s module allows you to provide custom configuration for a specific controller.

Model Layer

The final part of the MVC paradigm is the model. Sails comes with an advanced ORM/ODM component called Waterline. It was initially designed as a part of the Sails framework and later extracted into a separate Node.js module that can now be used independently.

Waterline provides an abstraction layer that connects your application to a wide variety of databases transparently and seamlessly. The main idea is that you would define your application’s domain model as a set of related entities (JavaScript objects) and that entities are automatically mapped to the database’s underlying tables and/or documents. The interesting aspect of Waterline is that it supports related entities between several databases. For example, you could store users in the PostgreSQL database and related orders in the MongoDB; the abstraction layer would be able to fetch them for you without your even noticing the difference.

Waterline is a pretty big component, and I’m not able to fully cover it in this introductory article, but I will try to give you the taste of it.

Suppose we are creating a simple app to manage contacts. Our app will have just two types of entities: a person and their contact information. The end user would be able to create a person and add multiple contact details for them.

Each separate database system that you would use in your Sails project requires a connection specification. The connections are configured in the config/connections.js file. I’m going to use a special database type called sails-disk. This database adapter is actually built into Sails, and it stores all data in a simple JSON file. This can be very useful when you are starting to design an app and have not yet selected or deployed a real database server.

Let’s now open the config/connections.js file and configure our connection:


module.exports.connections = {
  main: {
    adapter: 'sails-disk'
  }
};

This short configuration is enough for the sails-disk adapter. However, in a real-world scenario, you would need to provide all of the details required to connect to the database system of your choice — for example, the host name, port number, database name, username and so on.

Also, we would need to configure the model layer to use the specified connection by default for each model that we define. Open the config/models.js file and change its content to the following:


module.exports.models = {
  connection: 'main',
  migrate: 'alter'
};

The migrate property controls how Sails rebuilds the schema in your underlying database when a model definition changes. When it’s set to alter, Sails will try to update the schema without losing any data each time while the application lifts. The drop could also be a viable option in some cases — then, Sails will just recreate the schema every time the app is lifted. In a production environment, Sails will use the safe option, which will not make any changes to the schema at all. This really helps with protecting the fragile data in the production database. In safe mode, you will need to execute the migration manually. Leaving the migrate option undefined is also possible. In this case, Sails will ask you for an interactive choice every time when a migration is required.

Now, we are ready to define our models. Let’s use Sails’ built-in generator to create model files for us. Just issue these commands:


sails generate model person
sails generate model contact

Sails will create two basic files. Let’s edit them.

First, open the generated api/models/Person.js model and edit it:


module.exports = {
  attributes: {
    firstName: {
      type: 'string',
      size: 128,
      required: true
    },
    lastName: {
      type: 'string',
      size: 128
    },
    contacts: {
      collection: 'Contact',
      via: 'person'
    }
  }
};

Here, we are defining three fields: firstName, lastName and the contacts collection to hold the contact details. To define a many-to-one relationship between two models, we need to use two special properties. The collection property holds the name of the related model. The via property tells Waterline what field of the related model will be used to map back to this model. Hopefully, this is pretty self-explanatory.

Also, the size property specifies the maximum length of the string in the database column, and the required property specifies which columns may not contain null values.

Let’s edit the second model in api/models/Contact.js file:


module.exports = {
  attributes: {
    type: {
      type: 'string',
      enum: ['mobile', 'work', 'home', 'skype', 'email'],
      required: true,
      size: 16
    },
    value: {
      type: 'string',
      size: 128,
      required: true
    },
    person: {
      model: 'Person',
      required: true
    }
  }
};

Here, we’re defining yet another three fields. The type field will hold the type of the contact information. It could be a mobile number, a home phone number, a work number, etc. The additional enum property specifies the list of accepted values for this field. The value field holds the corresponding value. And the person field, mentioned earlier, maps the contact model to its parent person model through the special model property.

Note: We are not defining any primary keys or ID fields in our models. Waterline handles that for us automatically. The form of the ID’s value will depend on the database adapter being used because each database system uses different strategies to generate unique keys.

Also, Waterline will create two additional fields for each model, called createdAt and updatedAt. These fields hold the dates of when the entity was created and updated, respectively.

This behavior can be configured through the model options.

Using Sails’ Console To Test The Models

Sails offers a very nice interactive console that immerses the developer in the depth of an application’s context and that runs any JavaScript code we like.

The models are now defined, and we can use Sails’ console to test them and learn some basic APIs of Waterline.

Run the following command to launch Sails’ console:

sails console

After the console has launched, we can type in and execute some JavaScript in the context of our application. This is a quick way to test some aspects of a project.

First, let’s create some entities. Just type the following code into Sails’ console and execute it:


Person.create({ firstName: 'John', lastName: 'Doe' }).exec(console.log);

The Person here is the model we defined earlier (Sails exposes all models globally for your convenience). The create() is the method that creates new entities of the specified models; it takes an object with the field’s values as an argument. Make sure to correctly specify all required fields. Finally, the exec() method actually runs the required operations on the underlying database. It takes a single argument, the callback function, which will be called when the action completes. The created entity gets passed to it as a second argument. We are using the convenient console.log function here to output the newly created entity to the console.

The result should look as follows:


{
  firstName: 'John',
  lastName: 'Doe',
  createdAt: '2015-05-07T22:01:26.251Z',
  updatedAt: '2015-05-07T22:01:26.251Z',
  id: 1
}

See how the unique ID was assigned to the entity and additional fields were added with the actual dates.

Next, let’s create two contacts:


Contact.create({ type: 'mobile', value: '+7 123 123-45-67', person: 1 }).exec(console.log);
Contact.create({ type: 'skype', value: 'johndoe', person: 1 }).exec(console.log);

Make sure to specify the required person field with the proper ID value. This way, Waterline will know how to relate the entities to each other.

The final thing to do is fetch the created person, as well as the collection of its child contacts:


Person.find(1).populate('contacts').exec(console.log);

The find() method finds entities of the specified model; by passing 1 to it, we are telling Waterline to find the person entity with the ID of 1. The populate() method fetches the related entities; it accepts the name of the field to fetch.

It should return the person entity with all of its child contact entities as a traversable JavaScript object.

Note: I suggest that you experiment now and create multiple entities. As part of your experiment, see how validation rules are enforced by omitting some required fields or by using an incorrect enum value.

Of course, use Waterline’s documentation to your advantage!

Shadow Routes For Models

The Blueprints component, mentioned earlier when we talked about controllers, also comes into play with models. Again, it makes the developer’s life easier with two useful features: automatic REST and shortcut routes for our models.

By default, the Blueprints API provides implicit (shadow) routes for each model, with a defined controller. For this to work, we need to create empty controllers for our models. Just create two files, api/controllers/PersonController.js and api/controllers/ContactController.js, with the following content:


module.exports = {
};

After that, restart the application.

Now, with the help of the Blueprint API and its shortcut routes, we can enter the following URLs in the browser:

URLDescription
`/person/create?firstName=John&lastName=Doe`to create a new person entity
/person/find/2to get the person with the ID of 2
/person/update/2?firstName=Jamesto update a person with the ID of 2, giving it a new first name

These shortcut methods could be pretty useful during application development, but should be disabled in a production environment. I will show you how to do exactly that in the “Environments” section of this article.

Another, and probably the most useful, part of Blueprints is the automatic support for the REST APIs. The following implicit routes are provided for CRUD operations:

HTTP MethodURLDescription
POST/personcreates a new person
GET/person/2gets a person with ID of 2
PUT/person/2updates a person with ID of 2
DELETE/person/2deletes a person with ID of 2

Let’s create a new person using the REST API provided. I’ll use the great application for Google Chrome called Postman. It’s free and extremely useful for working with different HTTP APIs.

Select the POST HTTP method. Enter the URL https://localhost:1337/person, and provide the following JSON “raw” request body:


{
  "firstName": "John",
  "lastName": "Doe"
}

Make sure to select application/json as the request’s content type.

Now, hit the “Send” button.

Sails should satisfy your request and return a new entity with the freshly generated ID: STATUS 201 Created.


{
  "firstName": "John",
  "lastName": "Doe",
  "createdAt": "2015-05-13T21:54:41.287Z",
  "updatedAt": "2015-05-13T21:54:41.287Z",
  "id": 4
}

Note: I would recommend experimenting with these methods now. Try to create a new person and some contacts. Update the contacts to assign them to a different person. Try to delete a person. What happens with their associated contacts?

Every implicit Blueprint API route will be provided only if the model’s controller lacks the required action. For example, when you’re getting a single entity, the Blueprint API will look for an action called findOne. If such an action is not present in your model controller, then the Blueprint API will implement its own generic version of it. However, when an action is present, it will be called instead. Let’s create a very simple example just for the sake of demonstration: api/controllers/PersonController.js:


module.exports = {
  findOne: function (request, response) {
    Person.find(request.params.id).exec(function (error, persons) {
      var person = persons[0];
      person.fullName = person.firstName + ' ' + person.lastName;
      response.json(person);
    });
  }
};

This is a very simplified example of how such an action could work. All it does is fetch the required entity from the database and generate a new field called fullName from the first and last name of the person; then, it just returns a JSON result.

Be advised: This is a simple example that doesn’t handle errors or edge cases properly.

The complete list of all REST operations that are supported by the Blueprint API can be found in the official documentation.

Environments

Sails supports multiple execution environments; the built-in ones are development and production. When you run sails lift, it runs your app in the development environment by default. In other words, it’s equivalent to running sails lift –dev. You can also execute sails lift –prod to run your application in the production environment.

Multiple environments are provided to make the developer’s life easier. For example, in a development environment, some caching functionality is disabled by default in order to always return fresh results. Also, Sails will look for changes in your assets directory and will recompile assets in real time using its Grunt task.

We can take this concept further and use it to our advantage.

Each environment can override the application configuration to make it behave differently. If you look in your config directory, you will find a subdirectory named env. It contains custom configuration files for each environment. By default, these files are empty (not counting the comments).

Let’s configure our application to use port 80 in a production environment and also disable the Blueprint API’s shortcut methods. Open the config/env/production.js file and change its content:


module.exports = {
  port: 80,
  blueprints: {
    shortcuts: false
  }
};

Now, start Sails using the following command:

sudo sails lift --prod

Here, sudo is required in order to bind to the privileged port. Also, make sure that the port you’ve specified is not used by some other web server, like Apache 2 or nginx. If you can’t start Sails this way for some reason, just replace the port with something else, like 8080, and run the command again without the sudo.

Now, your Sails app should listen on port 80, and all shortcut requests like https://localhost/person/find/2 should not work. However, the REST API should work as expected.

You can also check the current environment in your code dynamically and adjust the business logic according to it. The name of the current environment is stored in the global sails.config.environment property. Here’s an example:


if ('production' == sails.config.environment) {
  // Actually send the email only in production environment.
  sendEmail();
}

Final Words

In this introductory article, I’ve shown you the most important parts of the Sails framework and given you some specific examples to get you going. Of course, if you want to use it in your daily work, you will have to spend some time mastering it and taking it to the next level. The good news is that Sails comes with pretty solid documentation and an active community. The creator of Sales even answers questions on StackOverflow personally. You will not be alone.

And remember, constant self-education and exploration is key to success. When you get some good results with Sails, feel free to come by and help the developers make it even better.

I’m hoping to continue writing about more specific aspects of Sails to give you an even deeper understanding of the framework itself and the Node.js ecosystem as well. Stay tuned!

Further Reading

Smashing Editorial (al, ml, rb, jb, mrn)