In the previous chapters, we created a Sails application and
customized it to our needs. We also saw how we can change some of the
default configurations provided by Sails.
Before we move forward, I’d like to discuss some of the
functionalities that Sails provides out of the box. Personally, I think
some of these functionalities are extra. By extra, I
mean that they are useful, but they don’t necessarily need to be part of
the framework.
This chapter is part of my book, Understanding and Using Sails.js, which I'm writing and publishing chapter by chapter.
You can read the first few chapters for free on this blog. Visit the book's dedicated page for more information, or head over to Gumroad to purchase a copy and/or support the ongoing writing of this book.
I believe a framework is nothing more than reusable code and some
common conventions that its creators decide on our behalf. The idea is
to save us from writing the same code again and again. But does every
useful piece of code need to be part of the framework?
Let’s take a look at some of the functionalities Sails provides and
see what happens when we don’t need them.
---
Blueprint is one of the interesting features of Sails. It
automatically provides routes and actions for our models, so we can
perform common CRUD operations without writing the code ourselves.
Let’s understand this with an example. Go ahead and run the
application first. We have the Contact model. So, in your
browser (or GUI API client), visit the following URL.
http://localhost:3000/contact/create?firstName=Kai&lastName=Doe
You should see a response like this:
{
"id": 1,
"firstName": "Kai",
"lastName": "Doe"
}
This is what I meant when I said that Blueprint automatically
provides routes and actions for our models, allowing us to perform
common CRUD operations without writing the code ourselves. If you
now visit the home page, you will notice that we’re already
seeing the result of Contact.find() in the console.
For a given model, Sails provides Blueprint actions such as
find, findOne, create,
update, replace, destroy,
populate, add, and remove without
requiring us to write them ourselves. For example, to delete the contact we just created, visit the
following URL in your browser.
http://localhost:3000/contact/destroy/1
It will return the deleted contact. If you visit the same URL again,
you’ll get a 404 response because the contact with that ID no longer
exists.
{
"id": 1,
"firstName": "Kai",
"lastName": "Doe"
}
Sounds good, right? But what if we want to write all those operations ourselves? We can
do that, too. We can define our own routes and controller actions and
write whatever logic we need. That’s what we did when we defined a root
route that logs the result of the Contact.find()
method.
Of course, Blueprint provides much more than this simple example. It
saves us from writing repetitive code, and it can be useful when we’re
building an API quickly. But I don’t think it’s essential to the
framework itself. If we don’t need it, we can disable Blueprint routes in
config/blueprints.js.
We don’t have a config/blueprints.js file yet. Let’s
create one.
touch config/blueprints.js
Open blueprints.js and write the following code.
module.exports.blueprints = {
actions: false,
rest: false,
shortcuts: false,
};
These options allow us to disable different types of Blueprint
routes:
actions disables action shadow routes.
rest disables REST blueprint routes.
shortcuts disables shortcut routes.
With these options set to false, Sails won’t
automatically create those Blueprint routes. Our explicitly defined
routes will continue to work.
One thing worth mentioning: shortcut routes are intended for
development convenience and should not be enabled in production. They
allow us to perform operations through URLs without writing our own
routes and controllers.
---
Sails comes with a built-in asset pipeline. It uses Grunt to process
assets and move them into the directory from which they can be served to
the browser. This is very useful when we want to compile Sass into CSS,
combine JavaScript files, or minify assets before deploying our
application.
But those days are mostly gone. Have you witnessed many new projects
using the traditional approach, where we build a web application using
an MVC framework, write SCSS alongside our views, or use CoffeeScript
instead of JavaScript? Nowadays, backends are often used to provide APIs, while frontend
applications are developed separately. And even if you disagree with
that approach, the ecosystem has moved towards other solutions such as
Webpack, Vite, esbuild, Parcel, and so on.
I don’t have a problem with Grunt or similar
solutions. I personally like these tools and technologies. The problem
is that developers are moving quickly towards newer solutions. So
quickly that many developers don’t want to learn older, but proven and
mature, technologies.
I’ve witnessed projects that were beautifully written,
well-maintained, and used for a long time, only to see the company plan
to rewrite them using the next popular technology because it was
becoming difficult to find developers who knew the existing
technology.
Again, we can use a separate tool to manage our frontend assets. We
can use Vite, Webpack, or any other tool we prefer. We can even keep our
frontend application in a separate project.
If our Sails application only provides an API to a React, Vue, or
mobile application, we might not need the built-in asset pipeline at
all. Sails allows us to disable the Grunt hook. For example, we can add
the following configuration to .sailsrc.
{
"hooks": {
"grunt": false
}
}
This disables the built-in Grunt hook. We can now manage asset
compilation outside Sails.
One thing to remember: disabling the hook also means Sails won’t
perform the asset processing that the hook normally handles. If our
application depends on those tasks, we need to replace them with another
solution.
---
Realtime communication is another feature Sails provides out of the box. Sails uses Socket.IO to provide realtime communication between the server and clients. It uses WebSocket when possible and provides additional functionality on top of it. For example, imagine we're building a chat application. When one user sends a message, we want to deliver it to other connected users without asking them to refresh the page. Realtime communication is useful for this kind of application.
But what if we're building a simple REST API? Do we need WebSocket support? No. We can build the entire application using regular HTTP requests. For example, we can define a route that returns a list of contacts and consume it using fetch() from the frontend. There is no need to establish a persistent socket connection.
If we don't need socket support, we can disable the sockets hook through .sailsrc.
{
"hooks": {
"sockets": false
}
}
This disables Sails' built-in socket functionality. Our regular HTTP routes can still do their job.
I like the fact that Sails provides realtime communication without requiring us to build everything from scratch. But I also think it should remain optional. Not every application needs it.
---
Now let's talk about Node Machines. Node Machines provide a standard way to define reusable functions, describe their inputs, and specify their possible outcomes. The idea is to make functions more predictable and easier to reuse across projects. For example, imagine we want to add two numbers. We can write a regular JavaScript function.
function add(a, b) {
return a + b;
}
That's perfectly fine. We don't need a framework to add two numbers! But let's say we want to define the inputs and describe the possible outcomes in a standard way. We can use the machine package.
You don't have to install the package or write the code yourself. It's here just for explanation.
First, install it.
Now let's define our function as a machine.
const Machine = require("machine");
const add = Machine.build({
identity: "add",
inputs: {
a: {
type: "number",
required: true,
},
b: {
type: "number",
required: true,
},
},
exits: {
success: {
outputType: "number",
},
},
fn: function (inputs, exits) {
return exits.success(inputs.a + inputs.b);
},
});
The function now has a defined interface: it expects two numbers and returns their sum through the success exit. This is a small example, but the same idea can be useful when building larger reusable operations.
This looks good in theory, especially in a small example where the code itself explains everything. But would you write this much code when building an application? Do you think this JavaScript code is readable? I don't think so. We can use ordinary JavaScript functions, promises, and async/await instead.
Node Machines are an additional programming model. Some developers might like the structure they provide, while others might prefer plain JavaScript. Personally, I'd rather have this kind of functionality available as a separate package instead of making every developer learn it as part of the framework.
---
Internationalization, often shortened to i18n, is the process of preparing an application to support multiple languages and locales. For example, we might want to show Hello, world! to English-speaking users and નમસ્તે, દુનિયા! to Gujarati-speaking users. We could write our own translation functions and store translations in JSON files. Alternatively, we could use an existing internationalization package. Sails provides this functionality out of the box.
Let's see how it works.
First, let's create a config/i18n.js file and define the languages our application supports.
Open config/i18n.js and write the following code.
module.exports.i18n = {
locales: ["en", "gu"],
defaultLocale: "en",
};
We're telling Sails that our application supports English (en) and Gujarati (gu). English is our default language. Now, we need to provide the translations. Sails looks for translation files in the config/locales directory by default. Let's create two files: en.json and gu.json.
In the en.json file, write the following:
{
"hello": "Hello, world!"
}
Write the following in the gu.json file.
{
"hello": "નમસ્તે, દુનિયા!"
}
Notice that both files use the same key, hello, but provide different translations.
Now let's open our views/home.ejs file and replace its existing heading with the following code.
<h1><%= __('hello') %></h1>
That's it! We don't need to write our own translation function. Sails provides the __() function, which looks up the translation for the current locale. Restart the application and visit the home page. By default, you should see the English greeting. If the browser sends a Gujarati language preference, Sails can display the Gujarati greeting instead.
We can test this using curl, too.
curl -H "Accept-Language: gu" http://localhost:3000/
The response should contain the Gujarati greeting, assuming the root route renders our home.ejs view.
We can also explicitly change the locale for a particular request using req.setLocale(). For example, if we want to force Gujarati for the current request, we can do this in a controller action before rendering the view.
module.exports = {
index: async (req, res, next) => {
const contacts = await Contact.find();
console.log("contacts:", contacts);
const logs = await Log.find();
console.log("logs:", logs);
req.setLocale("gu");
res.render("home");
},
};
This is useful when we want to let users choose their preferred language instead of relying entirely on their browser settings. But does every application need internationalization? Probably not. If we're building an internal API that only returns language-neutral data, we might not need translation support at all.
I don't think internationalization is a bad feature to have. I just think that it should be possible to use it when we need it, without making it a requirement for every application.
---
We've looked at several functionalities that Sails provides and discussed whether they need to be part of the framework. Some of them, such as Blueprint, can save us from writing repetitive code. Others, such as realtime sockets and asset compilation, are useful only for certain kinds of applications. Node Machines provide another way to structure reusable functions, while internationalization helps us support users who speak different languages.
I don't think there's a single correct answer to whether these features belong in Sails. Framework design involves trade-offs, and different developers will make different choices. What matters to me is that we understand what the framework provides, why it provides it, and whether we actually need those features in our applications. After all, we're using a framework to make our work easier, not to give ourselves more things to learn without a good reason.
And that's exactly why one should understand the framework beyond simply using it!