DEV Community

Priscillia Aforah
Priscillia Aforah

Posted on • Updated on

Building a Node.js APIs with Sails.js and Fauna

Building a Node.js APIs with Sails.js and Fauna

Alt Text

Building APIs and user interfaces with JavaScript can be an advantage for small and large teams. However, building easy to deploy and scalable Node.js API to power your frontend, web or mobile application is not an easy task for any developer.

In this article will learn how to build scalable and production-ready Node.js web API using Sails.js framework - an MVC framework for Node.js. We will be building user management endpoints for a web API service. We will create endpoints for the following features:

  • User registration
  • User login
  • Forget/reset password

Let's get started by first introducing Sails.js

Prerequisites

To better understand this article, knowledge of server-side development and Node.js is necessary.

What is Sails.js?

Sails.js or Sails is a real-time MVC framework for building scalable production-ready Node.js applications. Created by Mike McNeil in 2015, Sails take inspiration from Ruby on Rails MVC framework. Sails come out of the box with features like:

  • Websockets - This is used for real-time communication and is suitable for real-time chat apps and more.
  • Waterline - An ORM for Node.js used for a data layer of your application faster and more scalable.
  • A CLI tool, Sails that aids you to scaffold a new sails application, generate boilerplate to speed up development server, controller actions and database migration.

Organisations and companies like Microsoft, Philips, Amazon and Verizon currently use sails to build web APIs for their various clients.
It's important to note that that Sails is built on top of Express.js, a framework of Node.js

Getting started

To get started with web API in Sails, you will need to install the Sails CLI tool. To do this, you will use the command below:

npm install -g sails
Enter fullscreen mode Exit fullscreen mode

The code block above will install Sails CLI globally on your local machine. To verify that Sails is installed, you can run the code block:

sails -v
Enter fullscreen mode Exit fullscreen mode

The code block above would return a version number if the install was successful.

Creating a new Sails application

To create a new application with Sails, let's run the command below.

sails new {project name}
Enter fullscreen mode Exit fullscreen mode

The command above takes in flags; this is used to tell Sails what boilerplate code to generate for our application. So let's run the code block below on our terminal to create a new Sails app

The command above is used to create a new Sails application; the --no-frontend is used to tell sails, not to generate assets and views. The --fast flag is used to create a Sails app and skip dependency installations.
cd into your project directory and open the application in your text editor of choice. We can do this using the command below.

cd sails_api && code .
Enter fullscreen mode Exit fullscreen mode

Your project structure should look like the image below.

The image above is boilerplate code generated by Sails for building web APIs; some of the files in the directory include:

  • api: Perhaps the most used file in Sails, this is where you will be creating most of your API logic; it contains controllers, models, and helpers subdirectories.
  • controllers: This is where you add controllers for your application.
  • helpers: this folder contains helper functions for your application. Helpers are reusable code that follows the node-machine specification.
  • models: this contains your waterline models, which are also database schemas.
  • config: this is where you define your app configurations; inside, it is where you define your routes, your database adapter for Waterline and your policies and actions.

You can learn more about Sails files and directories from Sails docs here.

Creating our User endpoint

In this section, we will create our user endpoint to do this. First, navigate into your config/routes, where we will be, defining the request type and location. So open yourconfig/routes.js` and add the code block below

"GET /": "home/index"
Enter fullscreen mode Exit fullscreen mode

The code block above is for a GET request assigned to the index action in the home controller that will handle this request.
Our routes.js file should look like the image below:

Next, let's create an action for our index request, to do this we will use Sails to create a controllers/home directory. To do this, run the code block below:

sails generate action home/index
Enter fullscreen mode Exit fullscreen mode

The code snippet above will create a home folder in the controllers directory, and an index action should be in it. The index.js should look like the image below:

To complete our home route, we need to add a response when a request is made to the / endpoint, update your home/index.js file to look like the code block below:

module.exports = {
  friendlyName: 'Index',
  description: 'Index home.',
  inputs: {
  },
  exits: {
  },
  fn: async function (_, exits) {
    // All done.
    exits.success({message: 'Sails API'});
  }
};
Enter fullscreen mode Exit fullscreen mode

Sails uses the exits.success method to return a success response for a request; with this when a user visits the /route, a JSON message is sent as a response.

Create a New User endpoint

In this section, we will be doing the following

  • Setup database connection and create a new user
  • Store the user in the database
  • Create an error handling process in case of errors ## Setting up database connection

We will be using FaunaDB. You can check out the fauna documentation on how to get started with Fauna.
Sails comes with an official adapter for faunaDB for Waterline which we will use, you can install the adapter using the command below:

`npm install sails-fauna`
Enter fullscreen mode Exit fullscreen mode

Next, we need to add the name of the adapter and our database connection to our config/datastore.js.
You should have created your fauna database before now, if you haven't you can check here for help.
Update your config/datastore.js file to look like the code block below:

default: {
   adapter: 'sails-fauna',
   url: 'your fauna database url' //you can add your own database url here
 }
Enter fullscreen mode Exit fullscreen mode

After you've done this, navigate to your config/models.js and uncomment the code snippet below

id: { type: 'string', columnName: '_id' }
Enter fullscreen mode Exit fullscreen mode

If you've done this, restart your development server by running sails lift.
Moving on, let's create a Schema for our user model in the section.

Creating User model Schema

To create a schema in a fauna database, we can define it using FQl, a query language by Fauna. For our users collection, we will need the following documents, fullName, email and password. To create a user model, let's run the command below

sails generate model user
Enter fullscreen mode Exit fullscreen mode

The above command will create a file in the models directory called User.js, inside it let's add the code block below:

bcrypt = require('bcrypt');
SALT_WORK_FACTOR = 10;

var UserSchema = new Schema({
  fullName: {
    type: String,
    required: true
  },
  email: {
    type: String,
    required: true,
    unique: true
  },
  password: {
    type: String,
    required: true
  }
}, {timestamps: true});

UserSchema.pre('save', function(next) {
  var user = this;
  // only hash the password if it has been modified (or is new)
  if (!user.isModified('password')) {return next();}
  // generate a salt
  bcrypt.genSalt(SALT_WORK_FACTOR, (err, salt) => {
    if (err) {return next(err);}
    // hash the password using our new salt
    bcrypt.hash(user.password, salt, (err, hash) => {
      if (err) {return next(err);}
      // override the cleartext password with the hashed one
      user.password = hash;
      next();
    });
  });
 });


UserSchema.methods.comparePassword = function(candidatePassword, cb) {
  bcrypt.compare(candidatePassword, this.password, (err, isMatch) => {
    if (err) {return cb(err);}
    cb(null, isMatch);
  });
};
Enter fullscreen mode Exit fullscreen mode

Using FQl, we can add a schema, we defined the schema for our User schema, Last function was to encrypt a user's password before we store it.

UserSchema.pre('save', function(next) {
  var user = this;
  // only hash the password if it has been modified (or is new)
  if (!user.isModified('password')) {return next();}
  // generate a salt
  bcrypt.genSalt(SALT_WORK_FACTOR, (err, salt) => {
    if (err) {return next(err);}
    // hash the password using our new salt
    bcrypt.hash(user.password, salt, (err, hash) => {
      if (err) {return next(err);}
      // override the cleartext password with the hashed one
      user.password = hash;
      next();
    });
  });
 });
Enter fullscreen mode Exit fullscreen mode

To add a createAt and updateAt, we added timestamps: true.
If you've done all these, run sails lift to start our development and see our changes.

Creating Create User Controller

So we have our user models setup, next is to create a route and a controller also known as an action for creating new users. First, let's start with creating a route for it in our routes.js file below:

'POST /user/register': 'user/register'
Enter fullscreen mode Exit fullscreen mode

To create a register user action, run the command below

sails generate action user/register
Enter fullscreen mode Exit fullscreen mode

After we've done this, we need to define the data in the input field of our POST request, so add the code snippet below in your input:{}.

inputs: {
    fullName: {
      type: 'string',
      required: true,
    },
    email: {
      type: 'string',
      required: true,
      unique: true,
      isEmail: true,
    },
    password: {
      type: 'string',
      required: true,
      minLength: 7,
    },
  }
Enter fullscreen mode Exit fullscreen mode

In the code above, We added validation properties to the inputs, next for us will be to add exits which are the results for an endpoint. To add responses to our requests, add the code block below to your exits:{} in our register.js file:

 exits: {
    success: {
      statusCode: 201,
      description: 'New user created',
    },
    emailAlreadyInUse: {
      statusCode: 400,
      description: 'Email already in use!',
    },
    error: {
      description: 'Something went wrong, try again',
    },
  },
Enter fullscreen mode Exit fullscreen mode

It's important to note that before you use the fn async function in Sails, this is for catching errors. Next, let's create a user record below; Add the code block below after your exits:{} function.

fn: async function (inputs, exits) {
    // All done.
    try {
      const newEmailAddress = inputs.email.toLowerCase();
      let newUser = await User.create({
        fullName: inputs.fullName,
        email: newEmailAddress,
        password: inputs.password,
      });
      return exits.success({
        message: `${newUser}}account created successfully`,
      });
    } catch (error) {
      if (error.code === 'E_UNIQUE') {
        return exits.emailAlreadyInUse({
          message: 'Oops :) an error occurred',
          error: 'This email already exits',
        });
      }
      return exits.error({
        message: 'Oops :) an error occurred',
        error: error.message,
      });
    }
  },
Enter fullscreen mode Exit fullscreen mode

In the code above, we added email validations and responses for our exits requests. We will build our login endpoint in the next section.

Creating Login Endpoint

To create a login endpoint, first, we need to add a route for it in our routes.js file, let's do that below:

POST /user/login': 'user/login
Enter fullscreen mode Exit fullscreen mode

To create a user login endpoint, we will need to make sure that the user's email is registered. Run the command below in your terminal to create login actions:

sails generate action user/login
Enter fullscreen mode Exit fullscreen mode

To make sure that an email address is registered, we will need a policy called can-login.js in our policies directory. You can find out more about policies here.

module.exports = async function (req, res, proceed) {
  const { email } = req.allParams();
  try {
    const user = await User.findOne({ email: email });
    if (!user) {
      res.status(404).json({
        error: `${email} not found`,
      });
    } else {
      return proceed();
    }
  } catch (error) {
    res.status(401).json({ error: error.message });
  }
};
Enter fullscreen mode Exit fullscreen mode

In the code block above, we use Node.js findOne method to look for emails provided by the user on login. If the user isn't found in our database, we will return a 404 error code and message that the email not found. We also added a catch method in case an error occurs during the process.

Writing API policy

Policies, according to the Sils documentation, are added tools for authorisation and access control. Finally, we will create a policy that works with our action in our API, so let's add the code block below to our policies.js file.

"user/login": 'can-login'
Enter fullscreen mode Exit fullscreen mode

Now add the inputs and exits to your inputs:{} and exits:{} object:

inputs: {
    email: {
      type: 'string',
      required: true,
    },
    password: {
      type: 'string',
      required: true,
    },
  },

  exits: {
    success: {
      description: 'Login successful',
    },
    notAUser: {
      statusCode: 404,
      description: 'User not found',
    },
  },
Enter fullscreen mode Exit fullscreen mode

Similar to our last policy, we will use an fn function with a try/catch method to first; find the user with the email, add an exit for when the user is not found; second, check the password of the user and an exit fo when the user password is correct. Let's implement these below

fn: async function (inputs, exits) {
    try {
      const user = await User.findOne({ email: inputs.email });
      if (!user) {
        return exits.notAUser({
          error: `${inputs.email} not found`,
        });
      }


await sails.helpers.passwords
        .checkPassword(inputs.password, user.password)
        .intercept('incorrect', (error) => {
          exits.passwordMismatch({ error: error.message });
        });
      return exits.success({
        message: `${user.email} is logged in`,
        data: user,
        token,
      });
    } catch (error) {
      sails.log.error(error);
      if (error.isOperational) {
        return exits.operationalError({
          message: `Error logging in user ${inputs.email}`,
          error: error.raw,
        });
      }
      return exits.error({
        message: `Error logging in user ${inputs.email}`,
        error: error.message,
      });
    }
  }
Enter fullscreen mode Exit fullscreen mode

For a successful exit, we returned a message that the user is logged in and in the case of errors again, we used the try/catch method.

Conclusion

In this post, we have learnt how to create APIs with Node.js framework Sails.js. You can extend this application by adding authentication and deploying it to any provider of your choice. you can find the source code for this article on GitHub. Below are a few more resources to help:

Discussion (0)