Node.js is really cool, but the abundance of powerful frameworks that simplify the development of web applications for other programming languages was a significant deterrent to the popularity of the use of JavaScript on servers. Many people needed a simple tool, one like your favorite RoR, Yii or ASP .NET MVC. The community of JavaScript aficionados worked hard on correcting this unfortunate situation, and several interesting solutions emerged as a result. Each of them has their own arsenal of killer-features, but in all that diversity I was attracted by an ambitious project hidden under modest title Sails.js.
A tantalizing choice
Before Sails.js, I dissected and studied Derby, Meteor, small Rendr, unpretentious Geddy and even the exotic Tower. I had to work with all these things for several days, and I thought that Derby was the coolest. I nearly made up my mind to select it for the project, but the lack of proper documentation and the need to work with the code carefully to correct a simple error forced me to continue my search.
As soon as I had said goodbye to Derby, I came across Sails.js, very young, but with an impressive number of stars on GitHub. Then the project was in its infancy, but I really liked the desire of the developers to comply with the ideology of the great and terrible RoR, so I decided to learn more about it. I have to mention that during the time that I was writing this article (“And I take my time when I write and never meet any deadlines,” — Editor’s comment on behalf of I. Antonov) Sails.js changed quite a lot, but the developers always give enough time to writing the documentation, so shifting to the next version never causes any problems.
Cool features of Sails.js
The first thing I wanted to note is that Sails.js is primarily an MVC-framework. If you are familiar with any other popular frameworks (RoR, Yii, CodeIgniter, ASP .NET MVC), you will feel comfortable immediately after you get acquainted with the structure of the Sails.js project, because you will see all the folders familiar from MVC pattern: models, views, controllers and other things typical for such products.
[efspanel style=”” type=””]
[efspanel-header]
[/efspanel-header]
[efspanel-content]
Despite of its ideological and architectural similarity to RoR, Sails.js can boast a lower barrier to entry. Using it after Derby felt like driving a car with automatic transmission after a manual gearbox.
[/efspanel-content]
[/efspanel]
As for the killer features, there are really a lot of them here. For instance, full integration with socket.io. This means that developers using Sails.js can easily develop applications in real-time mode! They will not have to struggle with the integration or adjusting the work of the program with Node.js, as everything works quite correctly just out of the box.
Special emphasis has to be made on console utilities, which provide total automation for routine operations. You would like to create a new controller, wouldn’t you? No problem! Write a single command, and the draft for your controller is ready. What did you say? Along with the controller, you also need a model? Again, no problem, there is also a command for that. A little later we will see how it all works.
Something I particularly liked when I worked with Sails.js was its friendliness to other frameworks/libraries. This framework does not try to solve all possible problems on its own and delegates non-core tasks to auxiliary solutions. Thus, the problem of making Sails.js friendly to your favorite client framework becomes a no-brainer.
Sails.js is supplied together with quite a powerful ORM called Waterline. It is fast, easy to use and friendly to any database. Immediately out of the box, we are offered adapters for the most popular DBMS solutions: MySQL, PostgreSQL, Redis, and MongoDB give the possibility to use a usual file for data storage. Such a storage space is convenient to use in the development phase, and today we will see it on an example from practice.
I will show the most special feature of Sails.js at the very end. Just imagine: RESTful API Sails.js automates all the routine associated with the generation very well. In practice, this means that the framework can automatically create API for common operations (CRUD, pagination, search) with the model you created, with the routes set. To create a model here is that simple, and you can work with it immediately, without writing a single line of code. To tell you a secret, it is this feature that inspired me to get better acquaintance with Sails.js.
I can go on about the goodies of Sails.js for a long time, but I would not want to take the bread from the authors of [official website] (sailsjs.org). Therefore, for more information, I recommend that you visit their website, and in this article we’d better examine the potential by a case study.
Wind in our sails
In the creation of my project for this article I would like to show the key features of the framework and also provide an example that can become a solid foundation for your future projects. In the end, I decided to use the classics of the genre for JavaScript testing frameworks and demonstrate Sails.js by an example of the development of a simple task list.
Our next task list will be created with the use of a popular stack of technologies. The backend will be ruled by Sails.js, while the front-end will be made with my favorite AngularJS. Well, life should not seem all fun, so we arrange downloads of the required libraries on the client with the use of RequireJS. Get comfortable in your seat and take some pills against sea-sickness Let’s set on our journey!
Prepare the environment
To reach this goal, we need a number of tools. These are, first of all, Node.js, NPM and Sails.js. It is best to deal with these components in their native environment, that is, unix-like operating systems (Linux, OS X, BSD). In Windows, it is theoretically possible to set up this bundle, but I have personally discovered some weird errors lately.
If you do not have an installed Linux distribution set, then do not waste your time with virtuals and immediately use DigitalOcean, a cloud hosting service. The DO presets include a draft with the latest version of Ubuntu and customized Node.js, so preparing the entire necessary work environment is only a matter of a few clicks for you. This is the option I chose to use.
That is to say, make up your mind, get ready and set Sails.js with the help of the command sudo npm-g install sails.
Creating a new project
Before you give a command to form a new project, I would like to equip you with a [link to the repository with the finished project] (goo.gl/lVjEYG). The link is to my account in BitBucket, where you can easily get the finished project and clone it. I recommend you to do it immediately, as some code listings may be too long to be shown in this article.
Now the organizational issues are clear and we give a command to create the skeleton of a new project: sails new todo. And in this simple way, we have prepared the first draft for our application. Immediately go to the directory of the project and have a quick look at its structure.
I will not tell you for the hundredth time that controllers and other MVC entities should be located in the folders with the same names ;), but I would like you to focus on the directory called assets. Here we should put all the resources that are to be available to the client. I am using the word ‘resources’ to mean CSS/HTML/JS/ pictures and stuff.
At this stage, our project is ready to be launched, so enter sails lift command and go to your host from your browser (1337 by default). If all the components have been set correctly, you will see a standard welcome page.
Setting up RequireJS
We will bring in all the additional tools from the popular CDNs instead of storing them locally — in a real project, this would reduce the page load time. This is due to the fact that CDNs are used by many popular projects, so many users are likely to have Angular or Require in their cache. Therefore, it is not necessary to download a large file from the library once again.
Now let’s determine the purpose of RequireJS. If we do not go into the technical details, we may just say that this tiny lib can easily clean up the mess in the library, which always builds up in any modern project. RJS provides us with modularity and enables asynchronous file uploads. The advantages are obvious: our application loads faster and we know exactly where all the scenarios are connected.
OK, now let us determine the place to connect RJS library. It has to be connected to all the pages which will use third-party scripts. In our example, such a place will be the master page. Sails.js defines such entity as Layout. In the layer (the analog of master page from the world of ASP .NET MVC), we can determine the basic page template, and the variable part will be supplied from the concrete representation.
Sails.js does not require the use of a particular template engine, but in the current version the performance of layers is supported only in the standard EJS. It is not the most powerful one, but it is enough to be used in our example, as well as for the most common tasks.
Open the file views/layout.ejs, delete all its contents and write the basic layout of the page in Listing 1 into this file. Pay attention to the variables enclosed in <% -%>. We will transfer their value from the controller. Within the listing, there are two calls — title and body.
After the closing tag “body”, we connect the require.js library from the popular resource CDN CloudFlare. Note the data-main attribute in the tag script. This shows the path to the configuration file (main.js) which we are going to create.
Since we are making an AngularJS application, at the same time, let’s define its beginning in the body tag. To do this, use the ng-app directive. We won’t write too much about the AngularJS directives, because we (to be precise, it was me:)) have already written about that in in the July issue of Hacker last year.
Listing 1. Preparing layout.ejs
<!doctype html> <html> <head> <meta charset="utf-8"> <title><%- title %></title> <link rel="stylesheet" href="/styles/style.css"> </head> <body ng-app="todoapp"> <%- body %> </body> <script src="//cdnjs.cloudflare.com/ajax/libs/require.js/2.1.11/require.min.js" data-main="/js/main.js"> </html>
Configuring RequireJS
Let’s create a configuration file main.js in assets/js. In that file we have to specify the libraries that we need to connect to the project, and at the same time we need to take care of AngularJS initialization. The code of configuration file is shown in Listing 2.
Listing 2. Configuring RequireJS
window.name = 'NG_DEFER_BOOTSTRAP!'; require.config({ 'baseUrl': '/js', 'paths': { 'angular': '//ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular' }, 'shim': { 'angular': { 'exports': 'angular' } } }); require([ 'angular', 'app' ], function (angular, app) { angular.element(document.getElementsByTagName('html')[0]); angular.element().ready(function() { angular.resumeBootstrap([app.name]); }); });
Since we are going to take the modules from the CDN, it is necessary to register the path to them in the property of path. The shim section determines the module names (and, therefore, the names of libraries) that were not created in AMD (not to be confused with the processor manufacturer) style. Next, we perform manual initialization of AngularJS, and to learn more about this process you can read the corresponding article.
Preparing the hero
The module loader is configured, and now it is the turn of Angular. There is nothing special to configure, it is enough to determine the interrelationship, register the application as a module (i.e., in angular.module space) and describe the module that will load RequireJS by means of require directive.
Let’s create a controllers.js file in assets/js and define a new RJS-module in it with the code from Listing 3 (by the way, if someone is annoyed with the word “listing”, you can write about that to Igor directly, as I cannot do anything about it :). — Editor’s note). In this module we will employ Angular itself and register controllers in the angular.module global space. After that, all the declared controllers will be available during the execution of the application.
Next thing in the code is the registration of a new controller TodoCtrl, which has to be located in the directory assets/js/controllers. This controller will contain all the necessary logic for the work of our task list. If we need to create a new controller, we only have to add one single line to the file. It is really convenient!
Listing 3. Registering Angular controllers
define(function(require){ var angular = require('angular'), Controllers = angular.module('controllers', []); Controllers.controller('TodoCtrl', require('controllers/TodoCtrl')); return Controllers; })
The final step in the connection of AngularJS RJS-module is to define (file app.js in assets/js), which will register the todoapp application and pull our controllers into it. The code is shown in Listing 4.
Listing 4. Registering todoapp
define([ 'angular', 'controllers' ], function (angular) { app = angular.module('todoapp', ['controllers']); return app; });
Server Controllers
This is all the preliminary work on the client that we need, and now let’s perform a little magic on the server. Let’s agree that our application will consist of two pages. The first would be of purely informational character (the user will see it when addressing the full host name), and the second shall be in the form of a list of tasks. To implement the above, we will have to create an additional controller: sails generate controller welcome.
The command will create a draft of the future controller with the name of WelcomeController.js in api/controllers. Let’s open the file in the editor and add to it in module.exports:
index: function(req, res) { res.view('welcome', { title: "You are welcome!" }); }
In the terminology of Sails.js, here we define a new action (index). The result of its performance will be the rendering of a view named welcome (see method view). By the second parameter we transfer the object to view method, while the properties are transferred to presentations. We have only one, “title”, and its value will be displayed in the call of the template tag.
If we want control to be transferred immediately to such controller (i.e., when a user calls the root), you have to make changes to the routing. We open the routing.js file from config folder and replace the route “default” with
'/': { controller: 'WelcomeController', action: 'index' }
Here we determined the controller for the “/” route, as well as the action method. Now we only have to create the welcome.ejs presentation and run the project for intermediate testing. In the presentation I just wrote the greeting of a single line and posted a link to the controller responsible for the display of the representation in the form of the task list. Make similar steps and test the example in action (sails lift).
Models, Controllers and API
Slowly but surely, we are approaching the final part of our adventure at the sea, and now it’s time to touch the generator API. To describe the “problem” entity we need a separate task model and the controller with the same name. We will be able to generate all that stuff in one go with the help of the command: sails generate api task. The output will include the model, the controller, and announced API for CRUD operations.
Before the API test we’ll describe the model. There will be only two fields in it: title (string) and completed (Boolean). It is easy to guess that in the first one we will save the name of the task, and in the second one, its status (whether it has been performed or not). The description of the model is given in the fifth listing. The description of the model can include different validators. The supply includes validators for the validation of typical values (email, url, post code, etc.).
Listing 5. Task Model
attributes: { "title": { "type": "string", "required": true }, "completed": { "type": "boolean", "defaultsTo": false }
Here we are done with the issue of the model and so we can go on to the API testing. We run the project and enter the following queries in the address bar of the browser:
your_host:1337/task/create?title=Проверка&completed=false your_host:1337/task/create?title=еще одна задача&completed=false
The result of the performance of each of them will be the presentation of the data in the JSON-format. We have to mention that the server does not simply bring them back in a convenient form, but also stores them immediately in the storage. Since we do not connect the adapter for the work with the database, a usual file acts as storage. To check that the data is saved, run another query, and the result will be the records we added:
your_host:1337/task/find
Finishing Touches
The server part of our application is fully prepared, so we can only create a presentation to display the form of the task list, and write some client code to interact with the API of the server. Let’s start with the presentation. The markup is compact enough, but it is still too long for a magazine article. Create your own todoList.ejs file in the views directory and transfer the code from the repository to it.
All this code was demonstrated in our last year’s article about Angular (available on our website). Here I will only give a brief explanation. All our tasks are placed into todos array. All we have to do on the client side is to bring them all out by using standard guidelines and add the directives responsible for interaction with the user (add, delete, mark about the performance).
The client’s logic as such is described in the file assets/js/controllers/TodoCtrl.js. A small snippet of the file is shown in Listing 6. Now, an empty collection emerges in the visible space. Next, using the $http service, we make a query to the API we had formed earlier. When you start the application we need to get all the data, so we make the request to find.
The query has to retrieve a collection of items in JSON format. The result of the Get method is an add-on that has to be indexed. We start the cycle and there we perform the indexing. We put our indexed collection to $scope.
Next we go on in a similar manner. We define the addTodo() method, for which a binding is set in the presentation, and write the logic for the addition of a new entry into it. The first thing we need to do is to send data to the server. We’ll perform this operation with the use of the familiar method — Get. If we succeed with the operation, we add the entries to the client.
Other operations (deletion, editing) are performed in the same way, and only the name of the service method used is changed: $http. For example, data editing is implemented with the use of put, and deletion is done with delete. In a real application, you should also provide error handling, but that’s another story.
Listing 6. A fragment of a TodoCtrl controller
$scope.todos = []; $http.get('/task/find').success(function(data) { for (var i = 0; i < data.length; i++) { data[i].index = i; } $scope.todos = data; }); // Adding a new task $scope.addTodo = function() { if (!$scope.newTodo.length) { return; } $http.get('/task/create?title=' + $scope.newTodo).success(function(data){ $scope.todos.push({ title: $scope.newTodo, completed: false }); $scope.newTodo = ''; }); };
Let’s test the project
Now the development of the project is completed. Try running the application and test it thoroughly. To improve the performance of the project I recommend you to try to implement a simple registration on your own and add global error handling, which is actually quite easy.
Getting our sails down
We created a real operating application with the use of a popular stack of technologies. Except for the bulky listing with its layout, we did not write too much code. The routine was taken by the frameworks, and we only have to decide on the logic of the application as such.
Sails.js framework has proven to be fit for purpose and can now compete with some popular solutions in other languages. It is possible that in your next project you will be able to do with JavaScript alone.
[efspanel style=”” type=””]
[efspanel-header]
For coffee lovers
[/efspanel-header]
[efspanel-content]
Developers have different attitude to CoffeeScript: some cannot imagine a development process without it, others are annoyed and criticize it for numerous drawbacks. I remain neutral and often do without the help of syntactic sugar. If you do not share my opinion and are accustomed to CS, then you can continue to use it in Sails.js. To do this, you need to make two simple steps:
Install coffee-script package: npm install coffee-script –save
Add the following line to the project configuration file (app.js): require(‘coffee-script/register’);
argument to use for API generation (controllers, models) — coffee. For example, sails generate api mymodel – coffee.
[/efspanel-content]
[/efspanel]
Hi there. Great article. I have been trying to work with AngularJs in my sails app for sometime. When I call sails lift, and open up the browser’s console, I am being told that there is a
“Mismatched anonymous define() module: function (require){
var angular = require(‘angular’),
Controllers = angular.module(‘controllers’, []);”.
I was just wondering if there was a particular reason behind this?
Cheers
Can you, for gods sake, put the date the article is written at top? People need to know how relevant the stack in your article to today’s version.