Chapter 01: Sails as MVC Framework
We need a sandbox application in Sails framework in order to do some experiments. Let's call it contacts as it'll be a contact management system application.
The steps we're going to follow in order to create a Sails app are somewhat unusual. We're not going to use Sails CLI/Sails global package, instead will create a folder and install Sails as dependency.
mkdir contacts cd contacts npm init -y npm i -E sails
Let's create an app.js file and then write the following code.
const sails = require("sails") sails.lift();
We first required the sails package, and then called the .lift() method to start the server on the default 1337 port.
We can run the application using node app.js or let's add this as start script in the package.json. So that, we can use the npm start command for the sake of usual practice.
{
"name": "contacts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"sails": "1.5.18"
}
}
Let's run the start command and wait for the ship to render in the terminal! Then you can visit http://localhost:1337 in the browser.
npm start ... some debug logs ... info: info: .-..-. info: info: Sails <| .-..-. info: v1.5.18 |\ info: /|.\ info: / || \ info: ,' |' \ info: .-'.-==|/_--' info: `--'-------' info: __---___--___---___--___---___--___ info: ____---___--___---___--___---___--___-__ info: info: Server lifted in `D:\Playground\contacts` info: To shut down Sails, press <CTRL> + C at any time. info: Read more at https://sailsjs.com/support.
Ah! We get the 404 - Not Found error in the browser. That is expected as we don't have code against the root route. I wish instead of 404 - Not Found error Sails should render something similar to Sinatra (hint: Sinatra render a page with code snippet to copy-paste) as guided learning journey. Next.js also provide the guided journey via console errors. Nice :)
---
Let's fix 404 - Not Found error. First, we need to define the root route. In Sails, routes are defined within config/routes.js file. We don't have config folder and routes.js file. Let's create both.
mkdir config touch config/routes.js
Open this file and write the following code.
module.exports.routes = {
'GET /': 'HomeController.index'
};
Eventually, we'll have lot more files in this config folder.
Note: If you're in the config folder, don't just write module.exports but a module.exports.[filename] such as module.exports.routes or module.exports.globals for the globals.js file.
routes is an object with key as HTTP verb + white space + path. The value should be a controller file + dot + function name within controller. If given route is visited, a function within controller file will going to be executed.
All the controllers in Sails are within api/controllers folder. We don't have api, controllers folders, and HomeController.js file. Let's create.
mkdir api mkdir api/controllers touch api/controllers/HomeController.js
Open this file and write the following code.
module.exports = {
index: (req, res, next) => {
res.send("hello, world");
}
};
If you know the Express then this code looks familiar to you! It is. Sails internally uses Express framework for all the base functionalities.
We now have the root route and the code to run against it. So with these changes, we are ready to run the app again. We should see hello, world text on the root route.
---Waterline is the database ORM built by the same team who built the Sails framework. Because of that, Waterline has seamless integration supports with Sails.
Waterlines supports few databases such as MySQL, PostgreSQL, MongoDB, in-memory database via adapters. For example, sails-disk package is in-memory database adapter. I'm going to use this one for the sandbox demos. You know what, I'm going use sails-hook-orm package as it has sails-disk package within it and more so that I don't have to write any configuration code.
npm i -E sails-hook-orm
We should have a contacts table in database. To work with contacts table, we need to create a Contact model. All the models in Sails are within api/models folders. We don't have models folder and Contact.js file. Let's create.
mkdir api/models touch api/models/Contact.js
Open this file and write the following code.
module.exports = {
tableName: 'contacts',
attributes: {
id: {
type: 'number',
autoIncrement: true
},
firstName: {
type: 'string',
required: true
},
lastName: {
type: 'string'
}
}
};
The code is self-explanatory. We're creating a contacts table with id, firstName, and lastName as columns with given attributes. For now, don't worry about the these attributes and their values.
We can have the Sails application without model/database support. But, If we want to use model/database support, we need to explicitly tell Sails that we're going to use database in our app. For that, we need to set models option to true in global options. The global options are defined within config/globals.js file. We don't have this globals.js file. Let's create it.
touch config/globals.js
Open this file and write the following code.
module.exports.globals = {
models: true
};
Theoretically, this should work. But, due to some internal awkward configuration, you'll get bunch of error if you try to run the app such as - _ / lodash needs to be false, or async needs to be false, or sails needs to be true and so on. To save you with trouble, add few more configurations in globals.js file.
module.exports.globals = {
models: true,
_: false,
async: false,
sails: true
};
Now if you run the app, it will not throw any error now. Go ahead and run the app. You should see following text in the console.
Excuse my interruption, but it looks like this app does not have a "migrate" setting configured yet. (perhaps this is the first time you're lifting it with models?) Tired of seeing this prompt? Edit config/models.js. In a production environment (NODE_ENV=production) Sails always uses migrate:'safe' to protect against inadvertent deletion of your data. But during development, you have a few different options: 1. FOR DEV: alter wipe/drop and try to re-insert ALL my data (recommended) 2. FOR TESTS: drop wipe/drop ALL my data every time I lift Sails 3. FOR STAGING: safe don't auto-migrate my data. I will do it myself Read more: sailsjs.com/docs/concepts/models-and-orm/model-settings#?migrate -------------------------------------------------------------------------------- What would you like Sails to do this time? ** NEVER CHOOSE "alter" or "drop" IF YOU ARE WORKING WITH PRODUCTION DATA ** prompt: ?:
Sails is now waiting for us to answer for the migration strategy. The possible answer we need to provide is 1, 2, or 3. Meaning of each is written in the console text. We can let's say enter 1 and press Enter to continue with our app. But, the problem is, each time you run the app, it'll ask this question again and again. To stop this, we need add our answer in config file.
The config file we need for this is config/models.js file. We don't have this globals.js file. Let's create it.
touch config/models.js
Open this file and write the following code.
module.exports.models = {
migrate: 'alter'
};
With the config saved in config/models.js file, it'll not ask again the same question! Run the application again and you should notice the following console log somewhere.
info: ·• Auto-migrating... (alter) info: Hold tight, this could take a moment. info: ✓ Auto-migration complete.
Perfect! contacts table is created in the database and you're ready to do the CRUD operations on this table using Contact. For example, open the HomeController.js file and add following lines within index function.
module.exports = {
index: async (req, res, next) => {
const contacts = await Contact.find();
console.log("contacts:", contacts);
res.send("hello, world");
}
};
I've converted the function to async function and then calling Contact.find() method. Re-run the application and you should see:
contacts: []
Meaning that database is configured correctly.
One last point before we move to views. We now have the .tmp/localDiskdb/contacts.db (and archive.db) file as we've decided to choose in-memory database.
Instead of returning plain hello, world text let's return hello, world as h1 header. To do so, we need to create a folder with name views and then we should have an EJS/template file within it e.g. home.ejs for root route.
We don't have views folder and home.ejs file. Let's create both.
mkdir views touch vies/home.ejs
Open this file and write the following code.
<h1>hello, world</h1>
Then we can adjust the index function in HomeController.js file to render this template instead of sending the plain hello, world text.
module.exports = {
index: async (req, res, next) => {
const contacts = await Contact.find();
console.log("contacts:", contacts);
res.render("home");
}
};
Re-run the app again! You should see hello, world as the header text!
I think we should stop here for the Chapter 01 as we touch base the MVC concepts in Sails. I would like to highlight that Sails provide MVC functionalities out of the box with just few lines of code and some uncommon adjustment.
If we can adjust these things we have nice framework!