Quickly Build Node.js Apps with Sails.js

Quickly Build Node.js Apps with Sails.js

Sails.js is an exciting MVC framework for Node.js. Recently the team released version 1.0 and today you’re going to learn how to use the framework to rapidly build a new website. The hype around Sails.js is real. As a veteran MVC developer, I was impressed with the extensibility, organization, flow, and speed Sails.js provides.

The team at Sails. js has done an excellent job of abstracting the MVC portion of the codebase away from the ancillary tasks of database management, authentication, authorization, setting up middleware, and anything else you can imagine. Once your project is set up the way you see fit, the MVC portion of the project is very quick. In this tutorial, I won’t be going over everything in the setup because Sails.js supports almost any tools you use. Once you start work on your project, you’ll want to go through and understand how best to meet your needs. I highly recommend going over the Concepts Section on Sails’ website for more information.

One last note about Sails.js in general; the framework really shines with a project that is well thought through and ready for development. If you start working on a project and continuously flip between the policies, routes, models, views, and controllers the framework can get a little overwhelming. It’s much easier to have the list of routes already designed before implementing the code then moving on to the controllers and so forth.

In this project, you will build a small web application for monitoring and maintaining a garden. Recently, I completed an enclosed garden for my wife and grew interested in monitoring and maintaining it remotely using an Arduino. The system will have a controller for a fountain, a GSM module for communicating with this web application, and an array of peripherals for monitoring the general health of the garden. This application will be used to store and display the metrics on the garden’s health as well as take instructions for turning the fountain on and off.

Setup Okta

First, you need to set up a new application in Okta. Log into your developer console and click on Applications and then Add Application. Okta will ask you to choose your platform which you can select Web. This will bring you to the Application Settings section of the setup phase. Name your application something meaningful, I named mine Gardino. You will likely need to change the ports on your Base URIs, Login redirects URIs, and Logout redirect URIs. By default, Sails.js uses 1337 so change 8080 to 1337.

Application settings

Press Done and you’ll be able to review your setup before moving forward. Make note of your ClientId and your Client Secret as you will need these for your application.

Create Your Sails.js Application

With your Okta application complete you can start building your application. First, you will need to install Sails. js globally using npm.

npm install sails -g

Next, you can use the Sails generator in your directory to create a new app.

sails new gardunio

You will be shown a prompt asking if you want to create a Web App or Empty project. Choose the Web App option.

Choose a template

The Web App option will automatically load some dependencies that are pretty useful out of the box. These include bootstrap, vue, and parasails. Parasails is a small wrapper around Vue.js and Lodash that is useful if you choose to use Vue.js on your front end.

The downside of this approach is it will create a whole web app with pages and models that you don’t need. You can remedy this by removing some of the irrelevant files. First, delete the views/emails. Next, delete views/layouts/layout-email.ejs . Finally, you can delete everything in the views/pages folder except homepage.ejs.

Next, in the api folder you should delete the hooks folder. You can delete the contents of api/controllers , api/models , and api/helpers . Finally, delete api/responses/expired.js and api/policies/is-super-admin.js.

Last, you will want to delete the contents of the assets/js folder. You may be wondering why you deleted all those files rather than using the Empty option. It’s a matter of deleting some extraneous things, vs going through the work of adding in missing but critical components to the application. It’s a little bit easier to remove the web app features than add back the missing components.

Install Application Dependencies

Because the web app version of the sails.js generator comes pretty near ready to go, the packages you need to install are minimal. You will need the Okta Middleware for managing your authentication. You will also want to get dotenv for housing your environment variables.

npm i @okta/oidc-middleware@4.0.1
npm i @okta/okta-sdk-nodejs@3.2.0
npm i dotenv@8.2

Once these are installed, add a file called .env to your root folder and add the following values. The OKTA_ORG_URL will be the same as the URL you used to login into your developer’s console. In the instance below, that would be https://dev-1234124.okta.com. The OKTA_CLIENT_ID and OKTA_CLIENT_SECRET you should have retrieved from the developer’s console when setting up your Okta application.

Okta login

APP_BASE_URL = http://localhost:1337
OKTA_ORG_URL = {yourOktaOrgUrl}
OKTA_CLIENT_ID = {yourClientId}
OKTA_CLIENT_SECRET = {yourClientSecret}

Configure Your Sails.js Application

Now, you can start to configure your application. As mentioned before, this is where most of your code not related to the application workflow itself is.

With the .env file set up and your Okta application configured, you can configure the Okta middleware to help handle your authentication. To do so you will need to configure the Okta middleware, express-session, and define the order in which the middleware will be executed. This work takes place in the config/http.js file.

require("dotenv").config();

module.exports.http = {
  middleware: {
    order: [
      "cookieParser",
      "session",
      "bodyParser",
      "compress",
      "poweredBy",
      "router",
      "www",
      "favicon",
      "expresssession",
      "oktaInit",
    ],

    expresssession: (function () {
      var session = require("express-session");
      return session({
        secret: "some*crazy*long*random*string",
        resave: true,
        saveUninitialized: false,
      });
    })(),

    oktaInit: (function () {
      var ExpressOIDC = require("@okta/oidc-middleware").ExpressOIDC;

      var oidc = new ExpressOIDC({
        issuer: process.env.OKTA_ORG_URL + "/oauth2/default",
        client_id: process.env.OKTA_CLIENT_ID,
        client_secret: process.env.OKTA_CLIENT_SECRET,
        appBaseUrl: process.env.APP_BASE_URL,
        scope: "openid profile",
        routes: {
          login: {
            path: "/login",
          },
          loginCallback: {
            path: "/authorization-code/callback",
            afterCallback: "/Dashboard",
          },
        },
      });

      return oidc.router;
    })(),
  },
};

Configure Sails.js Routes and Policies

Next, you will define your routes as well as the policies associated with them. Routing in Sails.js is very flexible and powerful. . A full explanation of routing features and options is out of the scope of this tutorial, but you should know routing in Sails.js is very flexible and powerful. Replace config/routes.js with the following code.

module.exports.routes = {
  "/": { view: "pages/homepage" },

  "GET /dashboard": { action: "dashboard/index" },
  "GET /history": { action: "history/index" },

  "POST /users/logout": { action: "users/logout" },
  "POST /device/toggle-fountain": { action: "device/toggle-fountain" },
};

Here you are registering 5 routes. The first is the index page which will return a view called pages/homepage. You haven’t created this view yet but you will shortly. The homepage is a static HTML page, so there is no need to bind the route to an action. The home page will simply serve as a landing page with a login button.

The remaining two pages, however, require a model in order to render them, so you will need to connect those to an action. First, you have a dashboard page which will show the current conditions of the garden itself. The second is a history page which will show you the trends of your garden’s health.

Finally, you have 2 post routes; one logs out the user and the other is for toggling the fountain on or off.

You don’t want unauthorized users coming in and viewing your garden’s status or controlling your water fountain so you’ll need to add some authentication to the routes you want to protect. For this project, you will put all of your routes—except the homepage—under authentication. To do so, you will define policies in your config/policies.js file.

module.exports.policies = {
  "*": "is-logged-in",
  "/": true,
};

Your policies are fairly straightforward. All pages will look to is-logged-in to determine if the user is logged in. That is overridden for the '/' path. Setting the '/' path to true means that the page allows public access.

Finally, you need to implement the is-logged-in policy module. Open the api/policies/is-logged-in.js file. If you have deleted this file, create a new one named is-logged-in.js under api/policies. Replace the contents of this file with the following code.

module.exports = async function (req, res, proceed) {
  if (req.session.passport) {
    return proceed();
  }

  // Otherwise, this request did not come from a logged-in user.
  return res.unauthorized();
};

When the user logs in using Okta, the value of that user will be stored in the passport session object. You can check the request’s session object for the passport. If it is there, the user is logged in and you can use that object to collect data about the user if required. If the passport is missing the user isn’t logged in and you return an unauthorized response. The unauthorized response is defined in api/responses/unauthorized.js.

module.exports = function unauthorized() {
  var req = this.req;
  var res = this.res;

  sails.log.verbose("Ran custom response: res.unauthorized()");

  if (req.wantsJSON) {
    return res.sendStatus(401);
  }
  // Or log them out (if necessary) and then redirect to the login page.
  else {
    if (req.session.userId) {
      delete req.session.userId;
    }

    return res.redirect("/login");
  }
};

The code automatically generated by Sails.js checks if the request wants a JSON response. If that’s the case, this function will return a 401 to the client indicating the user isn’t logged in. If it is requesting a page, the current user is removed from the session and is redirected to the login route. The login route is defined in the http.js file as the login page for Okta. Therefore, routing the user to the login page will send them to Okta to sign in. After a successful login, the user is redirected to the Dashboard page.

Bootstrap Your Sails.js App

By default, Sails.js reads and writes data to a local disk, which is perfect for prototyping your application. Although this tutorial does not use an external database, it’s good to know the config/datastores.js file is used to configure your backend database of choice. The built-in ORM will perform CRUD operations against your datastore while being agnostic to whatever underlying storage you use. For example, you can configure your test environment using local storage, a staging environment using MySql, and a production environment using PostgreSQL. The application will switch seamlessly between the three without affecting the rest of your application’s code.

Your application needs some initial data for testing. At application startup, Sails.js executes the config/bootstrap.js module, making it an ideal place to inject seed data. Replace the contents of config/bootstrap.js with the following code.

module.exports.bootstrap = async function () {
  var date = Date.now();

  await GardenData.createEach([
    {
      readingDateTime: date,
      temperature: 75,
      moistureContent: 15,
      sunlight: 80,
      ph: 7.8,
      storedWater: 14,
      fountainStatus: false,
    },
    {
      readingDateTime: date - 60 * 1000,
      temperature: 74,
      moistureContent: 15.2,
      sunlight: 80,
      ph: 7.8,
      storedWater: 14,
      fountainStatus: false,
    },
    {
      readingDateTime: date - 2 * 60 * 1000,
      temperature: 74,
      moistureContent: 15.6,
      sunlight: 80,
      ph: 7.8,
      storedWater: 14,
      fountainStatus: false,
    },
    {
      readingDateTime: date - 3 * 60 * 1000,
      temperature: 73,
      moistureContent: 16,
      sunlight: 80,
      ph: 7.5,
      storedWater: 14.5,
      fountainStatus: true,
    },
    {
      readingDateTime: date - 4 * 60 * 1000,
      temperature: 73,
      moistureContent: 16.5,
      sunlight: 80,
      ph: 7.5,
      storedWater: 15,
      fountainStatus: true,
    },
    {
      readingDateTime: date - 5 * 60 * 1000,
      temperature: 73,
      moistureContent: 17,
      sunlight: 80,
      ph: 7.5,
      storedWater: 15.5,
      fountainStatus: false,
    },
  ]);
};

This is populating basic information into your database by mocking up some test readings that may have taken place over the last 5 minutes. This is the first time you will see the query language used by Sails.js. It is fairly intuitive and, if you’re a veteran node developer, most of it will look very familiar to you. The createEach function takes an array of GardenData and adds it to your local store. After lifting the application you will be able to see this data in the .tmp/localDiskDb/gardendata.db file.

Add Models to Your Sails.js App

Before you lift the application, however, you need to define the GardenData model. You should put your models in the api/models folder that Sails generated for you.

module.exports = {
  attributes: {
    id: {
      type: "number",
      autoIncrement: true,
    },

    readingDateTime: {
      type: "number",
      autoCreatedAt: true,
    },

    temperature: {
      type: "number",
    },

    moistureContent: {
      type: "number",
    },

    sunlight: {
      type: "number",
    },

    ph: {
      type: "number",
    },

    storedWater: {
      type: "number",
    },

    fountainStatus: {
      type: "boolean",
    },
  },
};

Here is where you define the columns on your table with their types and any attributes they carry. In this case, you told id to auto increment. By default, Sails.js will consider id to be the primary key. The readingDateTime field will also automatically be populated with the created DateTime of the row.

You also want to define a FountainRequest model in the api/models folder. This model is what the controller at the garden will read to check if it should turn the fountain on or off. The web application will have a toggle switch on it to call your device/toggle-fountain action which will add a new record to this table.

module.exports = {
  attributes: {
    id: {
      type: "number",
      autoIncrement: true,
    },

    dateTime: {
      type: "number",
      autoCreatedAt: true,
    },

    toggle: {
      type: "boolean",
    },

    respondedTo: {
      type: "boolean",
    },

    respondedAt: {
      type: "number",
      autoUpdatedAt: true,
    },
  },
};

The key is knowing when the request has been handled. If there are multiple unhandled requests, the application should only send the latest and mark all as respondedTo. But, using the autoUpdatedAt attribute on respondedAt, will automatically populate the respondedAt field when you have updated respondedTo.

Add Controllers to Your Sails.js App

If you’re familiar with MVC you are probably used to having a controller file with a list of actions in it. Sails.js recommends keeping each action in its own file and using the folder structure to define the controller. This is the approach you will take here. Start by adding 4 new folders to the api/controllers folder; dashboard , device , history , and users .

In the dashboard folder, add a file namedindex.js. As you may have guessed, this folder structure and file name together will define the route dashboard/index.

module.exports = {
  friendlyName: "Dashboard",

  description: "Displays the Garduino Dashboard",

  exits: {
    success: {
      viewTemplatePath: "pages/dashboard",
    },
  },

  fn: async function () {
    var data = await GardenData.find({
      where: {},
      sort: [
        {
          readingDateTime: "DESC",
        },
      ],
      limit: 1,
    });

    return {
      currentData: data[0],
    };
  },
};

This is the real beauty of Sails.js. Controllers are small and concise and don’t rely on any code that isn’t directly related to rendering and serving the view. A controller returns an object with a few properties and a function. The fn function is used to build and return a model. The exits property defines what to do with the model—such as a view to render.

In dashboard/index.js, the fn function uses the Sails.js ORM (currently configured for local file storage) to retrieve the latest GardenData reading. The exits property instructs Sails.js to render the pages/dashboard view using the given model.

Next, you can add the index.js file to the history folder and add the following code.

module.exports = {
  friendlyName: "History",

  description: "Displays the Garduino History for the last Month",

  exits: {
    success: {
      viewTemplatePath: "pages/history",
    },
  },

  fn: async function () {
    var data = await GardenData.find({});
    return {
      gardenData: data,
    };
  },
};

Again, you are just returning some GardenData but this time you are getting all of the garden’s history. If you need to apply pagination, you can always use the skip and limit arguments to only send down a few of the entries at once.

Add a file for the logout.js action under the users folder. This will log the user out and redirect them to the home page.

module.exports = {
  friendlyName: "Logout",

  description: "Logs the User Out",

  exits: {},

  fn: async function () {
    this.req.session.destroy(function () {});
    this.res.clearCookie("connect.sid");
    this.res.redirect("/");
  },
};

Finally, under the device folder, create a file named toggle-fountain.js. This action will add an entry that will tell the fountain to turn off or on. The controller returns JSON data with a success flag.

module.exports = {
  friendlyName: "Toggle Fountain",

  description: "Logs a request for the Fountain to be turned on",

  exits: {},

  fn: async function () {
    var on = this.req.body.on == "true";

    await FountainRequest.create({
      respondedTo: false,
      toggle: on,
    });

    return this.res.json({ success: true });
  },
};

Add Views to Your Sails.js App

With all the server work done, it’s time to add the views. First, you should edit your layout view. In the layouts folder edit (or add) the layout.ejs file.

<!DOCTYPE html>
<html>
  <head>
    <title>Garudino - A Place to Manage your Garden</title>

    <!-- Viewport mobile tag for sensible mobile support -->
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, maximum-scale=1"
    />

    <%/* If you want to discourage search engines from indexing this site,
    uncomment the following line: */%> <%/*
    <meta name="robots" content="noindex" />
    */%>

    <!--STYLES-->
    <link rel="stylesheet" href="/dependencies/bootstrap-4/bootstrap-4.css" />
    <link rel="stylesheet" href="/dependencies/fontawesome.css" />
    <link rel="stylesheet" href="/styles/importer.css" />
    <!--STYLES END-->
  </head>

  <body>
    <div class="container">
      <nav class="navbar navbar-expand-lg navbar-light bg-light mb-4">
        <div class="collapse navbar-collapse" id="navbarSupportedContent">
          <ul class="navbar-nav mr-auto">
            <li class="nav-item active">
              <a class="nav-link" href="/"
                >Home <span class="sr-only">(current)</span></a
              >
            </li>
            <%if (req.session.passport) { %>
            <li class="nav-item">
              <a class="nav-link" href="/Dashboard">Dashboard</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="/History">History</a>
            </li>
            <% } %>
          </ul>

          <%if (!req.session.passport) { %>
          <div>
            <a class="btn btn-outline-success my-2 my-sm-0" href="/login"
              >Login</a
            >
          </div>
          <% } else { %>
          <div>
            <form action="users/logout" method="POST">
              <input type="hidden" name="_csrf" value="<%= _csrf %>" />
              <button
                class="btn btn-outline-success my-2 my-sm-0"
                type="submit"
              >
                Logout
              </button>
            </form>
          </div>
          <% } %>
        </div>
      </nav>

      <%- body %>
    </div>

    <!--SCRIPTS-->
    <script src="/dependencies/sails.io.js"></script>
    <script src="/dependencies/lodash.js"></script>
    <script src="/dependencies/jquery.min.js"></script>
    <script src="/dependencies/vue.js"></script>
    <script src="/dependencies/vue-router.js"></script>
    <script src="/dependencies/bootstrap-4/bootstrap-4.bundle.js"></script>
    <script src="/dependencies/cloud.js"></script>
    <script src="/dependencies/moment.js"></script>
    <script src="/dependencies/parasails.js"></script>
    <script src="/js/pages/dashboard.page.js"></script>
    <script src="/js/pages/history.page.js"></script>
    <!--SCRIPTS END-->
  </body>
</html>

There isn’t anything too groundbreaking on the view side. As this application is set up, you can use EJS or Vue to render your views. There is a login/logout button that detects if the user is logged in and some menu options that you hide from the user until they log in. You’ll also notice the <%- body %> tag that renders the body from the individual views.

Next is the home page. In your pages folder edit or add homepage.ejs.

<div class="jumbotron">

  <h3>Garduino</h4>
  <h4><i>Manage and Monitor your Garden Remotely</i></h4>

</div>

<div class="card">
<div class="card-header">
  Login In
</div>

<div class="card-body">

  <a class="btn btn-primary" href="/login">Using Okta</a>
</div>

</div>

You are providing the Login button for users as well as a small landing page.

Next, you can add history.ejs to your pages folder.

<div id="history">
  <h3>Historical Readings</h3>

  <hr />

  <div class="row">
    <div class="col-lg-12">
      <table class="table table-striped">
        <thead>
          <tr>
            <th>Date/Time</th>
            <th>Temperature</th>
            <th>Soil Moisture</th>
            <th>Sunlight</th>
            <th>Soil PH</th>
            <th>Stored Water</th>
            <th>Fountain Status</th>
          </tr>
        </thead>
        <tbody>
          <tr v-for="data in gardenData">
            <td>
              {{ new Date(data.readingDateTime).toLocaleDateString() + ' ' + new
              Date(data.readingDateTime).toLocaleTimeString() }}
            </td>
            <td>{{data.temperature}}&#730;</td>
            <td>{{data.moistureContent}}</td>
            <td>{{data.sunlight}}</td>
            <td>{{data.ph}}</td>
            <td>{{data.storedWater}} Gallons</td>
            <td>{{data.fountainStatus}}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </div>
</div>

<%- exposeLocalsToBrowser() %>

In this section, you present a table to the user with the gardenData information. Take note of the <%- exposeLocalsToBrowser() %> piece of code. This is provided by sails.js and provides a number of features. First, it will make your model available using window.SAILS_LOCALS. Second,it provides some protection against Cross-Site Scripting attacks by escaping untrusted data.

Where can you use window.SAILS_LOCALS? In the javascript page that is bound to this view. Under assets/js, add a new folder for pages. In that folder add a new javascript file called history.page.js and add the code below.

parasails.registerPage("history", {
  data: {
    gardenData: undefined,
  },

  beforeMount: function () {
    // Attach any initial data from the server.
    _.extend(this, window.SAILS_LOCALS);
  },

  methods: {},
});

This is where you will find all your client-side javascript for this page. Parasails uses registerPage to bind the javascript to the history section of your document. In order for your document to use it, you need to wrap your HTML/ejs/vue markup under a root div node with id history. Parasails will find the history div and bind this javascript to it. Then, you can attach the initial data from the server using _extend(this, window.SAILS_LOCALS) and make it ready to use for your view. Here, you’ve defined your data as gardenData and bound it to the gardenData that was returned from the action. Because this page doesn’t have much in the way of functionality there aren’t any methods here, but you can see on the dashboard page how those are used.

Add a new file under views/pages called dashboard.ejs and add the following code.

<div id="dashboard">
  <div class="row">
    <div class="col-lg-4 col-sm-12">
      <div class="card">
        <div class="card-header bg-danger">
          Current Temperature
        </div>
        <div class="card-body">
          <i class="fa fa-3x fa-thermometer-three-quarters text-primary"></i>
          <span class="pull-right h3">
            {{currentData.temperature}}&#730;
          </span>
        </div>
      </div>
    </div>

    <div class="col-lg-4 col-sm-12">
      <div class="card">
        <div class="card-header">
          Current Moisture Content
        </div>
        <div class="card-body">
          <i class="fa fa-3x fa-tint text-primary"></i>
          <span class="pull-right h3">
            {{currentData.moistureContent}}
          </span>
        </div>
      </div>
    </div>

    <div class="col-lg-4 col-sm-12">
      <div class="card">
        <div class="card-header bg-warning text-dark">
          Current Sunlight
        </div>
        <div class="card-body">
          <i class="fa fa-3x fa-sun-o text-warning"></i>
          <span class="pull-right h3">
            {{currentData.sunlight}}
          </span>
        </div>
      </div>
    </div>
  </div>

  <div class="row pt-4">
    <div class="col-lg-4 col-sm-12">
      <div class="card">
        <div class="card-header bg-secondary">
          Soil PH
        </div>
        <div class="card-body">
          <i class="fa fa-3x fa-balance-scale text-secondary"></i>
          <span class="pull-right h3">
            {{currentData.ph}}
          </span>
        </div>
      </div>
    </div>

    <div class="col-lg-4 col-sm-12">
      <div class="card">
        <div class="card-header bg-primary">
          Stored Water
        </div>
        <div class="card-body">
          <i class="fa fa-3x fa-chevron-up text-primary"></i>
          <span class="pull-right h3">
            {{currentData.storedWater}} Gallons
          </span>
        </div>
      </div>
    </div>

    <div class="col-lg-4 col-sm-12">
      <div class="card">
        <div class="card-header bg-info">
          Fountain Status
        </div>
        <div class="card-body">
          <i class="fa fa-3x fa-shower text-info"></i>
          <span class="pull-right h3">
            <label class="switch">
              <input
                @change="toggleFountain"
                name="on"
                type="checkbox"
                :checked="currentData.fountainStatus"
              />
              <span class="slider round"></span>
            </label>
          </span>
        </div>
      </div>
    </div>
  </div>
</div>

<%- exposeLocalsToBrowser() %>

<style>
  /* The switch - the box around the slider */
  .switch {
    position: relative;
    display: inline-block;
    width: 60px;
    height: 34px;
  }

  /* Hide default HTML checkbox */
  .switch input {
    opacity: 0;
    width: 0;
    height: 0;
  }

  /* The slider */
  .slider {
    position: absolute;
    cursor: pointer;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: #ccc;
    -webkit-transition: 0.4s;
    transition: 0.4s;
  }

  .slider:before {
    position: absolute;
    content: "";
    height: 26px;
    width: 26px;
    left: 4px;
    bottom: 4px;
    background-color: white;
    -webkit-transition: 0.4s;
    transition: 0.4s;
  }

  input:checked + .slider {
    background-color: #2196f3;
  }

  input:focus + .slider {
    box-shadow: 0 0 1px #2196f3;
  }

  input:checked + .slider:before {
    -webkit-transform: translateX(26px);
    -ms-transform: translateX(26px);
    transform: translateX(26px);
  }

  /* Rounded sliders */
  .slider.round {
    border-radius: 34px;
  }

  .slider.round:before {
    border-radius: 50%;
  }
</style>

Next, under assets/js/pages add a new file for dashboard.page.js and add this code.

parasails.registerPage("dashboard", {
  data: {
    currentData: undefined,
  },

  beforeMount: function () {
    // Attach any initial data from the server.
    _.extend(this, window.SAILS_LOCALS);
  },

  methods: {
    toggleFountain: function (e) {
      $.post("device/toggle-fountain", {
        on: e.target.checked,
        _csrf: window.SAILS_LOCALS._csrf,
      });
    },
  },
});

As you can see, you are giving a method for toggleFountain that will make a call back to the server to input a new record.

Run your Application

Finally, you’re ready to start your application. From the terminal run sails lift and use a browser to navigate to localhost:1337.

Garduino Sails.js application

If you run into an issue or simply want to compare your code, the complete source code for this tutorial is available on GitHub.

Sails.js is a great tool for helping you quickly build and prototype websites. The configuration options are immense and far more than can be covered in this article. I highly recommend taking the time to work with Sails.js with many different frameworks. Once you become comfortable integrating your favorite tools with Sails.js it makes your development experience much quicker and more enjoyable.

Learn More About Building Apps with Node.js

If you’d like to learn more about Node.js, check out some of our other great tutorials!

We are always posting new content. If you like this content, be sure to follow us on Twitter and subscribe to our YouTube Channel.

Okta Developer Blog Comment Policy

We welcome relevant and respectful comments. Off-topic comments may be removed.