This time we learn to create a secure API REST with CRUD operations like CREATE, READ, UPDATE and DELETE.

This development include a public and private API, for security we use JWT for authentication and Bcrypt for hashing our passwords. The database engine is run by MongoDB.

First let’s go over the basics.

What is REST API, Node.js, JWT, Bcrypt, MongoDB and Express?

REST API: it’s an interface between systems that use HTTP protocol for obtain data and perform operations. In this case we will use the most important operations: POST, GET, PUT and DELETE.

Node.js: it’s a runtime environment based in JavaScript for the server side, is asynchronous and event oriented architecture based on Google’s V8 engine.

JWT: it’s an open standard RFC-7519 based en JSON to create a token that serves to send data between application and services, guaranteeing that is authentic.

Bcrypt: is a password hashing function, based on Blowfish encryption and incorporates a salt fragment to generate the hash associated with each password, preventing to identical passwords generating the same hash.

MongoDB: it’s a document-oriented NoSQL database, storing BSON data structures.

Express.js: is a framework designed for Node.js, it helps us to create web applications more efficiently.

What are we going to do?

  • User registry through a form, the required data: Name, Email and Password.
  • The user must authenticate with email and password.
  • To consume the protected routes, the user must send the token in the header.

Let’s start!!

First we gonna create the directory where we saved the project and then we run the command npm init for start the project.

Once run this command it creates a new file with the name package.json. This file contains the project configuration.

{
  "name": "simplesecureapirest",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT"
}

Install the following dependencies with the command: npm install

npm install express mongoose bcryptjs jsonwebtoken morgan
npm install nodemon -D

After install the dependencies the file package.json will contain the list of dependencies and devDependencies (as we specified for nodemon).

"dependencies": {
    "bcryptjs": "^2.4.3",
    "express": "^4.17.1",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^5.11.8",
    "morgan": "^1.10.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.6"
  }
}

MVC Pattern (Model View Controller)

It’s a style of software architecture that separates components into objects, one for the application data, another for the user view and control logic.

Creating the server

Inside the directory that we created at the beginning, create a new file server.js.

const express = require('express');
const morgan = require('morgan');
const pkg = require('./package.json');


const app = express();


// Settings
app.set('pkg', pkg);


// Middleware
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));


// Routes


// Welcome Route
app.get('/', (req, res) => {
    res.json({
        author: app.get('pkg').author,
        name: app.get('pkg').name,
        description: app.get('pkg').description,
        version:app.get('pkg').version
    })
})


app.listen(3000, () => {
    console.log('Server running on port: 3000')
});

And to validate that everything is correct, start the server with command:

npm run dev

Then we access the following address from any browser http://localhost:3000 it should answer the following:

// 20201224010027
// http://localhost:3000/


{
  "author": "CarlosVldz",
  "name": "simplesecureapirest",
  "description": "A simple API REST",
  "version": "1.0.0"
}

Creating models and entities with Node.js

Models are representations of the database and will represent a single record/document. In this case and a practical example we will use a collection to save the information of our users and another collection for books information.

Create the User model with its respective fields (when we create a new User we’re creating an instance of that model).

In the project directory we create the models folder and the User.js file.

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');


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


// Hash password before save in DB
userSchema.statics.encryptPassword = async (password) => {
    const salt = await bcrypt.genSalt(10)
    return await bcrypt.hash(password, salt)
};


// Compare password 
userSchema.statics.comparePassword = async (password, receivedPassword) => {
    return await bcrypt.compare(password, receivedPassword)
};


module.exports = mongoose.model('User', userSchema);

Creating controllers

In the directory of our project create the folder controllers and inside the controller for the model created in the previous step auth.controller.js.

In this controller we will define two methods to create or register users “signUp” and to authenticate or start session “login”.

const User = require('../models/User');
const jwt = require('jsonwebtoken');


exports.signUp = async (req, res) => {
    const { name, email, password } = req.body;


    const newUser = new User({
        name, email, password: await User.encryptPassword(password)
    })


    const savedUser = await newUser.save();
    console.log(savedUser);


    const newToken = jwt.sign({ id: savedUser._id }, 'secretKey', {
        expiresIn: 86400 // one day
    })


    res.status(200).json({ newToken })
}


exports.logIn = async (req, res) => {
    const userExist = await User.findOne({ email: req.body.email });


    if (!userExist) return res.status(400).json({
        message: 'User not exists'
    })


    const matchPassword = await User.comparePassword(req.body.password, userExist.password)


    if (!matchPassword) return res.status(401).json({
        token: null,
        message: 'Invalid password'
    })
    console.log(userExist)


    const token = jwt.sign({ id: userExist._id }, 'secretKey', {
        expiresIn: 86400
    })


    return res.json({
        _id: userExist._id,
        name: userExist._id,
        message: 'Auth Succesful',
        token: token
    })

}

Creating routes

We will proceed with the creation of the routes for the methods of the previous step, within our directory create the folder routes and the file auth.routes.js.

const express = require('express');


const router = express.Router();


const authCtrl = require('../controllers/auth.controller');


router.post('/signup', authCtrl.signUp);


router.post('/login', authCtrl.logIn);



module.exports = router;

Creating the CRUD for books collection

Will this we can create, read, update and delete data, inside the controllers folder create the file book.controller.js.

const Book = require('../models/Book');


exports.findAllBooks = async (req, res) => {
    try {
        const books = await Book.find();
        res.json(books)
    } catch (error) {
        res.status(500).json({
            message: error.message || "Something goes wrong retieving the tasks"
        })
    }
};


exports.createBook = async (req, res) => {
    try {
        const newBook = new Book({
            name: req.body.name,
            author: req.body.author,
            status: req.body.status ? req.body.status : false
        });
        const bookSaved = await newBook.save();
        res.json(bookSaved)
    } catch (error) {
        res.status(500).json({
            message: error.message || "Something goes wrong creating a book"
        })
    }
};


exports.findOneBook = async (req, res) => {
    const { id } = req.params;
    try {
        const book = await Book.findById(id)
        if(!book) return res.status(404).json({
            message: `Book with id ${id} does not exists!`
        });
        res.json(book)
    } catch (error) {
        res.status(500).json({
            message: error.message || `Error retrieving book with id: ${id}`
        })
    }
};


exports.deleteBook = async (req, res) => {
    const { id } = req.params;
    try {
        const data = await Book.findByIdAndDelete(id)
        res.json({
            message: `${data.name} - Book were deleted successfully!`
        })
    } catch (error) {
        res.status(500).json({
            message: `Cannot delete book with id ${id}`
        })
    }
}


exports.updateBook = async (req, res) => {
    const {id} = req.params;
    try {
        await Book.findByIdAndUpdate(id, req.body)
    res.json({
        message: "Book was updated successfully"
    })
    } catch (error) {
        res.status(500).json({
            message: `Cannot update book with id: ${id}`
        })
    }
}

Now create the model for books Book.js inside the folder models.

const mongoose = require('mongoose');


// Define schema
const bookSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        trim: true
    },
    author: {
        type: String,
        required: true,
        trim: true
    },
    status: {
        type: Boolean,
        default: false
    }
});

module.exports = mongoose.model('Book', bookSchema);

Create the route handler for the records of the books book.routes.js

const express = require('express');


const router = express.Router();


const bookCtrl = require('../controllers/book.controller');


router.get('/', bookCtrl.findAllBooks);


router.get('/:id', bookCtrl.findOneBook);


router.post('/', bookCtrl.createBook);


router.put('/:id', bookCtrl.updateBook);


router.delete('/:id', bookCtrl.deleteBook);


module.exports = router;

Modify the server.js file to add the new routes that we create in the last steps.

const express = require('express');
const morgan = require('morgan');
const mongoose = require('./config/database');
const pkg = require('../package.json');


const authRoutes = require('./routes/auth.routes');
const bookRoutes = require('./routes/book.routes');


const app = express();


// DB settings
mongoose.connection.on('error', console.error.bind(console, 'DB Connection Errror'));


// Settings
app.set('pkg', pkg);


// Middleware
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));


// Routes
app.use('/api/auth', authRoutes);
app.use('/api/books', bookRoutes);


// Welcome Route
app.get('/', (req, res) => {
    res.json({
        author: app.get('pkg').author,
        name: app.get('pkg').name,
        description: app.get('pkg').description,
        version:app.get('pkg').version
    })
})


app.listen(3000, () => { console.log('Server running on port: 3000')

});

Create the configuration file for our database connection and one middleware to validate our JWT, which will authorize us to create, modify and delete any book in our records. The routes to list one or all the books it’s not necessary provide a token to consult.

Inside the root directory create the config folder and database.js file.

const mongoose = require('mongoose');


// Config DB Connection
const mongoDB = 'mongodb://localhost/secureAPI';

mongoose.connect(mongoDB, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useFindAndModify: false,
    useCreateIndex: true
});
mongoose.Promise = global.Promise;


module.exports = mongoose;

Then create the middleware folder and inside the authToken.js file.

const jwt = require('jsonwebtoken');
const User = require('../models/User');


exports.verifyToken = async (req, res, next) => {
    try {
        const token = req.headers["x-access-token"];
    if (!token) return res.status(403).json({
        message: "No token provided"
    })
        const decoded = jwt.verify(token, 'secretKey')
        req.userId = decoded.id;


        const user = await User.findById(req.userId, { password: 0 })
        if (!user) return res.status(404).json({
            message: "No user found"
        })
        next();
    } catch (error) {
        return res.status(401).json({
            message: "Unauthorized"
        })
    }
}

Finally we modify our book.routes.js file to specify the protected routes.

const express = require('express');


const router = express.Router();


const bookCtrl = require('../controllers/book.controller');
const authToken = require('../middleware/authToken');


router.get('/', bookCtrl.findAllBooks);


router.get('/:id', bookCtrl.findOneBook);


router.post('/', [authToken.verifyToken], bookCtrl.createBook);


router.put('/:id', [authToken.verifyToken], bookCtrl.updateBook);


router.delete('/:id', [authToken.verifyToken], bookCtrl.deleteBook);



module.exports = router;

Testing our API

In my case I use Postman, but you can use Insomnia or any other tool that allows you to test REST services.

Let’s see some operations:

  • To list all the books

  • In case to provide an invalid token

  • In case to not provide any token

In the following link you can find the API documentation, which contains all the routes of our CRUD for books and login and registry of new users. https://documenter.getpostman.com/view/12403851/TVsxBRaR

You can find the full code on my GitHub.