Building CRUD API using the Restify Framework

Building CRUD API using Restify Framework


A lot of countless options exist for building APIs with JavaScript frameworks these days, and each with their own advantages.

This article will focus on explaining the steps to build an API with Create Read Update and Delete (CRUD) functions using the Restify Framework for Node.js.

The project we’re going to be building would be a mini MVP loan management API that enables you to Create Read Update and Delete debtors with authentication implemented using JSON Web Tokens (JWT).

Let’s get started.

Creating the Project

create a folder called “debtor-api” in your preferred directory then open up the folder in your favorite terminal and run the following command.

npm init -y

You should get a response that says:

Wrote to /path_to/debtor-api/package.json:

{
  "name": "debtor-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

Open up the folder in your preferred text editor (eg VS Code) and modify the package.json file to run nodemon, a package that watches your project for changes and automatically reloads your server (we’ll install this later). In order to do this, replace the contents of the package.json file with the code below:

{
  "name": "debtor-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {}
}

Next, run the below commands to install nodemon (adds live reload to the dev server), restify (the framework we’re working with), mongoose (an elegant MongoDB object modeling for nodejs), restify-errors (helps to handle errors elegantly) and mongoose-timestamps (adds a timestamps field to our collections).

npm i nodemon restify restify-errors mongoose mongoose-timestamp

Next, create a MongoDB database either locally or on the cloud with MongoDB Atlas and fetch your connection string.

Setting up configuration

Create a new file in your project root directory and call it config.js and put the following settings in the file:

module.exports = {
    ENV: process.env.NODE_ENV || 'development',
    PORT: process.env.PORT || 5000,
    URL: process.env.BASE_URL || 'http://localhost:5000',
    MONGODB_URI: 'mongodb//user:password@hosturl/databasename'
  
}

Make sure to replace the value of MONGODB_URI above with your connection string.

Setting up the server

Create a file called index.js in your root folder and write the following code:

const restify = require('restify');
const mongoose = require('mongoose');
const config = require('./config');
const rjwt = require('restify-jwt-community');
const server = restify.createServer();

//Middleware
server.use(restify.plugins.bodyParser())

//Protected Routes 
/* 
the line below helps protect all routes from unauthorized access 
i.e You need a token to perform actions on all routes except those in the unless({{path:[]}) array
*/
server.use(rjwt({ secret: config.JWT_SECRET }).unless({ path: ['/api/auth', '/api/debtors/','/api/debtors:id'] }));
server.listen(config.PORT, () => {
    mongoose.set('useFindAndModify', false)
    mongoose.connect(
        config.MONGODB_URI, {
            useNewUrlParser: true,
            useUnifiedTopology: true,   
    }
    );
});

const database = mongoose.connection;
database.on('error', (err) => {
    console.log(err)
})

database.once('open', () => {
    require('./routes/debtors')(server)
    require('./routes/users')(server)
    console.log(`Server running on Port: ${config.PORT}`);
})

Setting up the Debtor and User models

In your project folder create two subfolders called models and routes.

In the models‘ subfolder, make a new file called Debtor.js (make sure it starts with a capital D) and put the following code into it:

const mongoose = require('mongoose')
const timestamp = require('mongoose-timestamp')
const DebtorSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        trim: true
    },
    email: {
        type: String,
        required: true,
        trim: true
    },
    debt: {
        type: Number,
        required: false,
        default: 0
    },
    phone: {
        type: String,
        required: false,
        default: ''
    }
});
DebtorSchema.plugin(timestamp) //adds timestampsautomatically to the model in the database

const Debtor = mongoose.model('Debtor', DebtorSchema);
module.exports = Debtor; //exports the Debtor model for reuse

The code block above defines a mongoose schema called DebtorSchema it also specifies the model to have the following fields: name, email, debt and phone as well as timestamps (created_at and updated_at) fields whose creation and maintenance will be handled by mongoose automatically.

Next, create a similar file for our User model in the same models folder and name it User.js (uppercase U) then add the following code to create a UserSchema with email and password fields:

//models/User.js
const mongoose = require('mongoose');

const UserSchema = new mongoose.Schema({
    email: {
        type: String,
        required: true,
        trim: true
    },
    password: {
        type: String,
        required: true
    }
});

module.exports = mongoose.model('User', UserSchema); // exports the User model for reuse in other files

Adding Routes to the API

Let’s add routes and configure the methods allowed and then the actions we want those routes to perform.

In your root folder again, create a new folder named routes and inside the folder, create two files: debtors.js and users.js . In the debtors.js file put the following block of code:

const errors = require('restify-errors')

const Debtor = require('../models/Debtor');
module.exports = server => {
    // Fetch All Debtors 
    server.get('/api/debtors/', async (req, res, next) => {
       try {
        const debtors = await Debtor.find({}) //query to look for all debtors
        res.send(debtors); // send the result from the query above back to the user 
        next();
       } catch (error) {
           return next(new errors.InvalidContentError(error))
       }
    
    });

    // Add a new Debtor to list 
    server.post('/api/debtors', async (req, res, next) => {
        // Check if what is being passed is in JSON
        if (!req.is('application/json')) {
            return next(new errors.InvalidContentError("This API expects: 'application/json'"))
        }
        const { name, email, phone, debt } = req.body;
        const debtor = new Debtor({
            name,
            email,
            phone,
            debt
            

        });

        try {
            const newDebtor = await debtor.save() //save  a debtor to the database
            res.send(201) //201 means created
            next(); 
            
        } catch (error) {
            return next(new errors.InternalError(error.message))
        }
    });

    //Fetch a Single Debtor 
     server.get('/api/debtors:id', async (req, res, next) => {
       try {
        const debtor = await Debtor.findById(req.params.id)
        res.send(debtor);
        next();
       } catch (error) {
           return next(new errors.ResourceNotFound(`Hi, there seems to be no debtor with ID of: ${req.params.id}`))
       }
    
     });
    
    // Update The Details of an Exising Debtor
     server.put('/api/debtors:id', async (req, res, next) => {
        // Check if what is being passed is in JSON
        if (!req.is('application/json')) {
            return next(new errors.InvalidContentError("This API expects: 'application/json'")) // data must be in json format 
        }
       
        try {
            const debtor = await Debtor.findOneAndUpdate({_id: req.params.id}, req.body)
            res.send(200)
            next();
            
        } catch (error) {
            return next(new errors.ResourceNotFound(`Hi, there seems to be no debtor with ID of: ${req.params.id}`)) // this is called when a debtor with the ID does not exist
        }
        }
    });
    //Delete an Already Existing Debtor

    server.del('/api/debtors:id', async (req, res, next) => { 
    //restify uses 'server.del' instead of 'server.delete' for delete operations

        try {
            const debtor = await Debtor.findOneAndRemove({ _id: req.params.id }) // await a query to look for debtor with the given id
            res.send(204); // 204 response means no content 
            next()
        } catch (error) {
             return next(new errors.ResourceNotFound(`Hi, there seems to be no debtor with ID of: ${req.params.id}`)) //this is called when a debtor with the ID does not exist
        }
    })

}

Adding Authentication with JWT

To set up authentication for our application, we need a few npm packages namely: bcryptjs, restify-jwt-community and jsonwebtoken. Install them by running the command below

npm install restify-jwt-community jsonwebtoken bcryptjs

After the packages above have been installed, add the following property to the modules.export object in the config.js file you created earlier and replace ‘supersecretkey‘ with anything of your choice (Note: do not leave such exposed in a production environment’):

JWT_SECRET: process.env.JWT_SECRET || 'supersecretkey'

Create another file called auth.js in your routes folder and add the following code:

const bcrypt = require('bcryptjs');
const mongoose = require('mongoose');
const User = mongoose.model('User');

exports.authenticate = (email, password) => {
    return new Promise(async (resolve, reject) => {
        try {
            //try to fetch user
            const user = await User.findOne({ email })
            
            // Match User email with password
            bcrypt.compare(password, user.password, (err, isMatch) => {
                if (err) throw err;
                if (isMatch) {
                    resolve(user);
                } else {
                    // Password did not match
                    reject('Failed to authenicate user')
                }
            })
        } catch (err) {
            // Can't find user email
            reject('Sorry, Authentication failed')
        }
    });
}

Next, in the routes/users.js file add the following to set up the user routes and API methods:

const errors = require('restify-errors');
const bcrypt = require('bcryptjs');
const User = require('../models/User');
const auth = require('../routes/auth')
const jwt = require('jsonwebtoken');
const config = require('../config')

module.exports = server => {
    // Create a new User
    server.post('/auth/register', (req, res, next) => {
        const { email, password } = req.body;

        const user = new User({
            email,
            password
        });

        bcrypt.genSalt(10, (err, salt) => {
            bcrypt.hash(user.password, salt, async (err, hash) => {
                // Encrypt the password
                user.password = hash;

                //Save User to database
                try {
                    const newUser = await user.save();
                    res.send(201)
                    next()
                    
                } catch (err) {
                    return next(new errors.InternalError(err.message))
                }
            });
        })

    });

    //Authenticate a User
    server.post('/api/auth', async (req, res, next)=> {
        const { email, password } = req.body;
        try {
            const user = await auth.authenticate(email, password);
           // Create JWT
            const token = jwt.sign(user.toJSON(), config.JWT_SECRET, { expiresIn: '10m' })
            
            const { iat, exp } = jwt.decode(token);

            // respond with token
            res.send({iat, exp, token})
            console.log(iat, exp, token)
            next();
        } catch(error) {
            //Unauthorized
            return next(new errors.UnauthorizedError(error))
        }
    })
}

Running the App

To test-run our API open up your terminal and either of the following command:

npm run dev 
#or
nodemon index.js

Next, head over to Postman or any API testing tool of your choice and make requests to the API endpoints and you should get responses similar to these below:

Restify Framework
GET request to /api/debtors/
Restify Framework
an example POST request to /api/auth endpoint.

Conclusion:

Restify framework provides a fast and efficient way to build REST APIs using Nodejs and JavaScript and is fast becoming one of the most popular API development frameworks in existence today.

A flaw, however (in my opinion) is that it is not as MVC based (templating and rendering are not supported out-of-the-box) as other frameworks like Adonis, Express, Loopback.js, etc hence, you would want to integrate a frontend framework to handle the View rendering.

You can check out the source code for this tutorial on Github


Share on social media

//