Migrating from Rails to Sails: Comparison and Implementation Guide
Ruby is an awesome programming language and from my experience, the easiest to learn. As a web developer with Ruby background, there are chances that you write JavaScript for a better user-facing application. You are a smart Ruby developer (you probably love the DRY principle) and that’s why you make use of Rails MVC framework to build your Ruby apps.
Your life has been going so simple and happy until you started feeling left behind because there is a new tool that everybody is using and talking about—Node. From research, you have come to realize that Node is just JavaScript on the server and because of the performance features it comes with, it has gained massive popularity.
Together, we are going to approach Node from Rails perspective so it could feel familiar. Sails is a Node framework inspired by Rails, therefore, with your little JavaScript skills, we will build an application in Node using Sails while putting in practice everything we know about the Rails architecture.
This article is structured in such a way that Rails and Sails’ core concepts are discussed side by side starting from their background , down to their REST implementation. We have a long way to go so grab a cup of coffee and relax so we can have a better time learning.
Project Demo
Background
Rails
Rails is written in the Ruby programming language and was created by David Hansson. Rails was open sourced by David in 2004. The framework was designed using the MVC (Model-View-Controller) pattern for better separation of concerns. This particularly made Rails intuitive to use because as long as a developer is comfortable with Object-Oriented Programming and the MVC pattern, one can easily jump on Rails with basic understanding of Ruby concepts and start building awesome web products.
Sails
Sails is written in JavaScript, precisely Node.js. Sails framework was created by Mike McNeil and was inspired by Rails. This fact that Sails was inspired by Rails made them have a lot in common with each other—the architecture (MVC), directory structure, assets management, etc. For these reasons, if you are coming from Rails and have basic JavaScript skills, then you are good to start writing Node apps with Sails.
Version Managers & Installation
Version managers are useful when you have to build different kinds of projects on the same machine and the same platform but require different versions of the platform. For instance, you could have v.2.3 of Ruby on your machine but you are working with a team on a given project that strictly requires v1.9. The possible solutions to this are virtual machines (which, of course, are harsh on machine resources) or version managers.
Keeping in mind that we are going to build two projects side by side on different platforms (Ruby & Node), let’s install their version managers:
Ruby (Rails)
Ruby is known as Ruby Version Manager (RVM). To get RVM on our machine, we need to first install the author’s public key
:
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
Then we can go ahead to install RVM using curl
:
curl -sSL https://get.rvm.io | bash -s stable
You can run any RVM command to confirm installation:
rvm --help
With RVM installed, we can have multiple installations of Ruby versions but let’s just install one as default:
rvm --default install 2.3.1
The install command is used to install specific versions and the —default
flag sets the particular version as default.
You can confirm installation by running:
ruby -v
Node (Sails)
Node is known as Node Version Manager (NVM). We can install NVM
with the following curl
script:
curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.7/install.sh | bash
You need to add NVM to your path using:
# Add to path
export NVM_DIR="$HOME/.nvm"
# Load NVM
[-s "$NVM_DIR/nvm.sh"] && . "$NVM_DIR/nvm.sh"
We want to use the latest version of Node so to install, run:
nvm install node
Now we have installed both Ruby and Node, our platforms are ready.
Package Managers
Code re-use is beautiful but it’s more enjoyed when you re-use other’s. These days, the community of a given technology is calculated with the amount of contributions made by the community in mind. When building large projects, there is a tendency that you are going to rely on other people’s solution to enhance yours. The way these solutions are managed and kept in sync are through package managers.
Ruby (Rails)
Rails package manager is called gem
and you can install any gem
that is useful to you using the following syntax:
gem install <package-name>
Eg:
ruby install sass
Where sass
is the name of the package we want to install.
Rails itself is a gem
so we install it straight away:
gem install rails
Node (Sails)
Node’s package manager is npm
and comes installed when we install Node. npm
has much in common with gem
:
npm install <package-name>
Eg:
npm install express
Sails, just like Rails, is also a Node package and can be installed with:
npm install -g sails
The -g
flag tells npm
to install Sails globally so it can only be installed once and used for subsequent projects. If the -g
flag is ignored, Sails can be installed in the directory the command was run, which could be inside a folder labeled node_modules.
Setup
We have gone through the preparation process for a Rails and Sails project and now it’s time to set it up. Just a reminder, we will build the same project twice. One with Rails and the other with Sails. Because we are coming from a Rails background, we will first handle a task in Rails and then see how we can complete that in Sails.
Create a folder named cm-rails-sails
where cm
stands for code mentor
. It is inside this directory that both projects will live.
Rails
To create a new project in Rails, inside the created folder, run:
rails new todo-rails
Sails
Just as we have done for Rails, in the same cm-rails-sails
folder, run:
sails new todo-sails
Command Line Tool
Project scaffold and boilerplates can really be a pain in the skull because it is a repetitive process that just bores the brain to perform. Both frameworks provide command line tools for scaffolding projects (just as we have seen above) and generating a project’s pieces like controllers, models, etc.
Listing all the commands for both frameworks are irrelevant because trust me, you will never memorize them once. The best way to learn them is to encounter them through the learning process.
Directory Structure
Let’s have a look at the directory structure for both projects and see what we can make of it:
Rails
# Holds application core logic
|--app # Truncated to show the MVC folders
|----controllers
|----models
|----views
|--bin # Executables
|--config # Application specific configurations
|--db # Seeds Schema Migrations
|--lib
|--log # Application log files
|--public # Client facing contents
|--test # App's test files
|--tmp # Temporary contents like cache
|--vendor # Third party libs from package managers
|--Gemfile # Packages
|--Rakefile # Application specific commands
Sails
# Holds application core logic
|--api # Truncated to show the MVC folders
|----controllers
|----models
|--assets # Client facing contents
|--config # Application specific configurations
|--node_modules # Third party libs from package managers
|--tasks # Application specific commands
|--app.js # Entry
|--Gruntfile.js # Grunt task for assets
|--package,json # Packages
Sails looks simpler which would make it friendlier to approach. Rails is more matured because it has been around for a while and way older than Sails
Database Configuration
We will leave the database configuration as defaults which will leave us with sqlite
for Rails and disk for Sails:
Rails
# ./config/database.yml
default: &default
adapter: sqlite3
pool: 5
timeout: 5000
development:
<<: *default
database: db/development.sqlite3
Sails
// ./config/connections.js
localDiskDb: {
adapter: 'sails-disk'
},
Models and Migrations
Let’s have a look at the data layer of MVC — M. Both frameworks use the same approach to generate models, but the difference is what happens under the hood:
Rails
rails generate model Todo owner:string text:string
Sails
sails generate model Todo owner:string text:string
As you can see the only difference is rails
and sails
. Both commands generate the following models:
Rails
# ./app/models/todo.rb
class Todo < ApplicationRecord
end
# ./db/migrate/<time_stamp>_create_todo.rb
class CreateTodos < ActiveRecord::Migration[5.0]
def change
create_table :todos do |t|
t.string :owner
t.string :text
t.timestamps
end
end
end
Sails
// ./api/models/Todo.js
module.exports = {
attributes: {
owner : { type: 'string' },
text : { type: 'string' }
}
};
Sails do not require migration files to define its schema. The schema of a given model is defined right inside the model’s definition. Each model file must export an attributes
object. The attributes object defines the schema of a given model.
Rails require that you migrate by running:
rails db:migrate
On the other hand, migration is not required for Sails. Waterline ORM which is what Sails uses does the magic for you when you start hitting the database with requests.
Routing
Routing is a key aspect of every web application, so both Rails and Sails take routing matters to heart. Let’s create some routes on both projects:
Rails
# ./config/routes.rb
# Todo routes
get '/todos', to: 'todo#index'
get '/create', to: 'todo#new'
post '/create', to: 'todo#create'
# Home page
root 'todo#index'
We have created four routes to be handled by Todo
controllers which we are yet to create. Now let’s see how we can do the same in Sails:
Sails
// ./config/routes.js
module.exports.routes = {
// Todo routes
'get /todos': 'TodoController.index',
'get /create': 'TodoController.new',
'post /create': 'TodoController.create',
// Index page
'/': 'TodoController.index'
}
Sails uses an object’s key-value pairs to map the controller’s action methods to its respective routes. The first word in the keys describes the HTTP verb that will be used to take care of this request.
Controllers
We have our data layer in place and our routes are defined. Next thing to do is to use controllers to handle requests coming from routes. In doing so, we can either send data as response or receive data as request with the help of our models:
Rails
# ./app/controllers/todo_controller.js
class TodoController < ApplicationController
# index action method
def index
# View data
@todos = Todo.all
end
# new action method
def new
end
# create action method
def create
# Retrieve data from form body
@todo = Todo.new(params.require(:todo).permit(:owner, :text))
# Persist
@todo.save
# Redirect to home page after persisting
redirect_to '/'
end
end
We have created three action methods to handle our routes: index
, new
, and create
. The first two handles get requests so Rails can demand a presentation layer—the view. We will tackle that soon. The last handles form submission so no view is required, rather, we just redirect to the home page.
Sails
// ./api/controllers/TodoController.js
module.exports = {
// index action method
index: function(req, res) {
Todo.find().exec(function(err, todos) {
if(err) throw err;
// Render view with found todos
res.view('todo/index', {todos: todos});
});
},
// new action method
new: function(req, res) {
// Render todo form
res.view('todo/new');
},
// create action method
create: function(req, res) {
// Create a new todo using the Todo model
Todo.create(req.body).exec(function(err, todo) {
if(err) throw err;
// Redirect if successful
res.redirect('/');
});
},
};
The three methods implemented on Rails are now available in our Sails project. The index
method fetches all todos and renders a view with the todo. The new
method just renders a form in a file named new
. The create
method creates a new todo and redirects to the home page if the attempt was successful.
Views
From the previous section which we discussed controllers, it is easy to conclude that we just need two views in the todo app—a view to show all todos, and another to show a form to create a todo. At this point, if you try to run with rails server
or sails lift
, you have run into errors that try to tell you that the view template files are nowhere to be found. Let’s create them:
Rails
<!-- ./app/views/todo/index.html.erb -->
<h1>Rails Todos</h1>
<table>
<tr>
<th>Owner</th>
<th>Text</th>
</tr>
<% @todos.each do |todo| %>
<tr>
<td><%= todo.owner %></td>
<td><%= todo.text %></td>
</tr>
<% end %>
</table>
The index
view loops through the view data that was passed to it from the controller and presents the data using a table.
<!-- ./app/views/todo/new.html.erb -->
<%= form_for :todo do |f| %>
<p>
<%= f.label :owner %><br>
<%= f.text_field :owner %>
</p>
<p>
<%= f.label :text %><br>
<%= f.text_field :text %>
</p>
<p>
<%= f.submit %>
</p>
<% end %>
The new
view presents a form that posts to the /create
route so that the request can be handled by the create action method in the Todo controller.
Sails
<!-- ./views/todo/index.ejs -->
<h1>Todos</h1>
<table>
<tr>
<th>Owner</th>
<th>Text</th>
</tr>
<% todos.forEach(function(todo){ %>
<tr>
<td><%= todo.owner %></td>
<td><%= todo.text %></td>
</tr>
<% }); %>
</table>
<!-- ./views/todo/new.ejs -->
<form action="/create" method="post">
<input type="text" name="owner">
<input type="text" name="text">
<input type="submit" value="Submit">
</form>
The same thing we saw in Rails applies to Sails. As a matter of fact, the template engine for Sails, ejs
was inspired by the template engine of Rails, erb
.
REST (BONUS)
RESTful services have become the best way to go when building solutions that will be consumed by different platforms (PCs, Mobiles, IoT, etc). Before we wrap up our discussions, let’s quickly have a look on how to generate resource endpoints for REST and how to return JSON data rather than rendering views:
Rails
Add the following route in Rails route config:
# ./config/routes.rb
resources :todos
When we do so, we are served with a collection of routes that promotes building REST apps:
GET /todos(.:format) todos#index
POST /todos(.:format) todos#create
GET /todos/new(.:format) todos#new
GET /todos/:id/edit(.:format) todos#edit
GET /todos/:id(.:format) todos#show
PATCH /todos/:id(.:format) todos#update
PUT /todos/:id(.:format) todos#update
DELETE /todos/:id(.:format) todos#destroy
We can confirm this by running:
rails routes
If we architected our application in such manner, we would have to return JSON rather than views in our controller action methods’ responses:
def index
@todos = Todo.all
#Render data as JSON
render :json => @todos
end
Sails
The case is quite different in Sails. Sails uses automatic route binding for RESTful services. Let’s see for ourselves. Run:
sails generate api Todo
The above command generates a model (Todo.js
) and a controller (TodoController.js
) for our projects. But that is not all. Once we create an action method in the controller, Sails automatically binds the method name to a route named after the action. For instance, if we create an action method named new
, Sails will create a route todo/new
and bind it to new
.
Just like what we saw in Rails, we can now start returning JSON rather than rendering views:
index: function(req, res) {
Todo.find().exec(function(err, todos) {
if(err) throw err;
// Render data as JSON
res.json(todos);
});
},
Final Note
We tried as much as possible to touch the core concepts of MVC by comparing Rails and Sails side by side. You have gotten your hands dirty with this and I bet your feet are wet. For this reason, go ahead and keep building apps with Node using a tool that makes you feel like you are still writing Rails.
You are bound to encounter challenges but the community is broad with many experts that can help you, and within few minutes you are moving on with a perfect solution. Good luck writing more JavaScript.
Good practical basic comparison! Exactly what I was after.
probably throwing error in node is not a good idea. it can drop node process if not handled. is this case covered by Sails?