DEV Community

Ndifreke Friday
Ndifreke Friday

Posted on • Edited on • Originally published at voidnerd.com

Build a Nodejs Restful API with Express and MongoDB

This Article was first published on voidnerd.com/

In this tutorial, we are going to build an authenticated nodejs api with express, hash passwords with bcryptjs and secure your API with JWT.

Prerequisite

Make sure you have the following installed on your system

  • Nodejs >= v12
  • Mongo >= v4
  • Postman (To test your endpoints)

Now that we have the prerequisites out of the way, let's create a directory for our app, run below this bash command for this.

$ mkdir node-api && cd node-api
Enter fullscreen mode Exit fullscreen mode

NPM utility will walk you through creating a package.json file. Follow the prompt and press enter all through to use the defaults.

$ npm init
Enter fullscreen mode Exit fullscreen mode

Create Files and Folders in your node-api directory like so:

--model
-----user.js
-----index.js
--controllers
-----auth.js
-----user.js
-----index.js
--public
--.env
--middleware.js
--.gitignore
--routes.js
--server.js 
Enter fullscreen mode Exit fullscreen mode

Notice the .evn file; this is where we will store our sensitive values as enviromental variables.

.gitignore is where we specify files or folders we don't want git to track; this is so we don't push sensitive or unwaranted data to github (or your prefered source code repository).

public folder for serving your static files.

Install project dependencies

In your project directory run below command to install dependencies.

$ npm install express mongoose morgan parse-error dotenv bcryptjs jsonwebtoken indicative
Enter fullscreen mode Exit fullscreen mode

For further reading on these dependencies

Moving on...

Register.

Just like every other thing on earth, we are going to start with creation (pun intended). In this section, we will focus on setting up our app and enabling user creation. Less talk, more code, let's roll :) .

Add needed enviromental variables to your .env file like so:

MONGO_URL=mongodb://127.0.0.1:27017/nodeApp
JWT_SECRET=thisisasecretlongstring
Enter fullscreen mode Exit fullscreen mode

Add paths we would like git to ignore in .gitignore file like so:

node_modules/
.env
Enter fullscreen mode Exit fullscreen mode

Type in the following code in model/User.js .

const  mongoose = require('mongoose')
const  Schema = mongoose.Schema;
const  bcrypt = require('bcryptjs');
const  JWT = require('jsonwebtoken')

const  jwtSecret = process.env.JWT_SECRET

let  userSchema = Schema({
    name:  String,
    email: {
    type:  String,
    required:  true,
    unique:  true
    },
    password:  String
})

// hash passwords for new records before saving
userSchema.pre('save', function(next) {
if(this.isNew) {
    var  salt = bcrypt.genSaltSync(10)
    var  hash = bcrypt.hashSync(this.password, salt)
    this.password = hash
}
next();
});
//validate user password
userSchema.methods.validPassword = function(inputedPassword) {
    return  bcrypt.compareSync(inputedPassword, this.password)
}

//sign token for this user
userSchema.methods.getJWT = function() {
    return  JWT.sign({ userId:  this._id }, jwtSecret)
}
module.exports = mongoose.model('User', userSchema)
Enter fullscreen mode Exit fullscreen mode

In the above sample we are defining our user schema and hashing our password for new records(this.isNew in pre save hook) . We also made available validPassword method for validating passwords and getJWT method of retrived user specific token.

Add this to models/index.js, this will help us have access to all the modules in models folders when we import just the folder [e.g require('./models').

var  normalizedPath = require("path").join(__dirname);
require("fs").readdirSync(normalizedPath).forEach(function(file) {

    if(!file.includes('index')) {
        var  moduleName = file.split('.')[0];
        exports[moduleName] = require('./' + moduleName);
    }
});
Enter fullscreen mode Exit fullscreen mode

For our registration logic, add below code to controllers/auth.js

const { validate } = require('indicative').validator;
const {User } = require('../models')


exports.register = async (req, res) => {

    //Validate request data
    const  rules = {
        name:  'required|string',
        email:  'required|email',
        password:  'required|min:6|max:30'
    }


    validate(req.body, rules).catch((errors) => {
        return  res.status(422).json(errors[0])
    });
    try {
        const  user = new  User  //initialize mongoose Model
        user.name = req.body.name
        user.email = req.body.email
        user.password = req.body.password
        await  user.save() //save user record to database

        const  token = user.getJWT();

        // data { user, token } = data {user: user, token token}
        return  res.status(201).json({data: { user, token }});
    } catch (err) {
        //return error if user unique field already exists
        if(err.name === 'MongoError' && err.code === 11000) {

        field = Object.keys(err.keyValue)[0]
        const response = {
            message:  `${field} already exists!`, field:  field
        }   
        return  res.status(422).json(response)
    }

    return  res.status(409).json({message:  "error saving data"})

    }
}

Enter fullscreen mode Exit fullscreen mode

In the above code we validated our request with indicative, save out user record to mongodb and return the appropriate responses to our users.

Add this to constrollers/index.js to have access to all modules in controllers folder.

var  normalizedPath = require("path").join(__dirname);
require("fs").readdirSync(normalizedPath).forEach(function(file) {

    if(!file.includes('index')) {
        var  moduleName = file.split('.')[0];
        exports[moduleName] = require('./' + moduleName);
    }
});
Enter fullscreen mode Exit fullscreen mode

let's work on our routes on routes.js

var  express = require('express')
var  router = express.Router()

/* Import Controllers*/
const  controllers = require('./controllers');


/* Define all your routes*/

router.post('/register', controllers.auth.register)
router.post('/login', controllers.auth.login)


/*Export your routes*/
module.exports = router;
Enter fullscreen mode Exit fullscreen mode

See how neat our routes are, when we use controllers. I love it :)

Let's get down to our Server Logic. Add below code to server.js.

const  express = require('express')
const  app = express()
const  path = require('path') //native module, no need to install
require('dotenv').config()
const  logger = require('morgan')
const  mongoose = require('mongoose')
const  pe = require('parse-error')
const  routes = require('./routes')

const  port = 3000 // server starts on this port

//mongoose options
const  mongooseOptions = {
    useUnifiedTopology:  true,
    useNewUrlParser:  true,
    useCreateIndex:  true
}

//connect to database
mongoose.connect(process.env.MONGO_URL, mongooseOptions)
    .then(
    () =>  console.log('Database Connection established!'),
    err  =>  console.log(err)
)

app.use(logger('dev')) // For logging out errors to the console
app.use(express.json()) // for parsing application/json
app.use(express.urlencoded({ extended:  true })) // for parsing application/x-www-form-urlencoded
app.use(express.static(path.join(__dirname, 'public'))) // For serving static files
//use routes
app.use('/api', routes)

//handle unhandled error
process.on('unhandledRejection', error  => {
    console.error('Uncaught Error', pe(error))
    return
})

app.listen(port, () => {
    console.log(`Server Stated on http://localhost:${port}`)
})
Enter fullscreen mode Exit fullscreen mode

Change directory to your project and run the command

node server.js
Enter fullscreen mode Exit fullscreen mode

Test your api with postman using the register endpoint http://localhost:3000/api/register and you should get a successful response like the image below.

Register Endpoint Test

Login

Our Initial setup is going to be really helpful from here on out.

For our login logic, add code to controllers/auth.js just below your register logic

...
exports.login = async (req, res) => {
    const  rules = {
        email:  'required|email',
        password:  'required|min:6|max:30'
    }

    validate(req.body, rules).catch((errors) => {
        return  res.status(422).json(errors[0])
    });

    try {
        const  user = await  User.findOne({email:  req.body.email})

        if(!user) throw  new  Error("Invalid Email or Password")

        if(!user.validPassword(req.body.password) ) {
            throw  new  Error("Invalid Email or Password")
        }

        const  token = user.getJWT();

        return  res.status(200).json({data: { user, token }});

    } catch (err) {
        console.log(err)
        if(err) return  res.status(401).json({message:  err.message})
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above code we validated incoming request, checked if it's user has correct credentials and returned the appropriate response.

Now add this to your routes.js file just below your register route.

...

router.post('/login', controllers.auth.login)

...
Enter fullscreen mode Exit fullscreen mode

Ctrl C to stop your app if it is running and re-run it

$ node server.js
Enter fullscreen mode Exit fullscreen mode

Test your api with postman using the login endpoint http://localhost:3000/api/login .
Login Endpoint Test

User Profile

For our grand finale, we are going to have one route(user profile) of which users need the right access get a successful response.

Add this to middleware.js

const  JWT = require('jsonwebtoken');
const {User } = require('./models')

const  jwtSecret = process.env.JWT_SECRET

exports.auth = async (req, res, next) => {
    try {
        //get token from header: Bearer <token>
        const  token = req.headers.authorization.split(' ')[1];
        //verify this token was signed by your server
        const  decodedToken = JWT.verify(token, jwtSecret);
        ///Get user details
        let  user = await  User.findById(decodedToken.userId)
        if(!user) throw  Error("Unauthenticated")
        //put user in req object; so the controller can access current user
        req.user = user
        next();
    } catch {
        return res.status(401).json({
        message:  "Unauthenticated"
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

In the above code, we get the bearer token from the authorization header, verify the token, get user details and pass those details to the next function(controller).

Add below code to controllers/users.js

exports.currentUser = (req, res) => {
    return  res.status(200).json(req.user);
}
Enter fullscreen mode Exit fullscreen mode

And finally, your routes.js should look like the one below

var  express = require('express')
var  router = express.Router()

/* Import Controllers*/
const  controllers = require('./controllers');

/* Import Middleware*/
const  middleware = require('./middleware')

/* Define all your routes*/
router.post('/register', controllers.auth.register)
router.post('/login', controllers.auth.login)

router.get('/profile', middleware.auth, controllers.users.currentUser)


/*Export your routes*/
module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Ctrl C to stop your app if it is running and re-run it

$ node server.js
Enter fullscreen mode Exit fullscreen mode

Test your protected endpoing http://localhost:3000/api/profile .
Auth Endpoint Test

Tip: Pay attention to the Headers section and the Authorization value.
Tip 2: Get your token from a successful login/register response.

Where do I go from here? Well there are a lot more ways we could improve our current API; you could add bonus routes like: delete user, get all users, get single user, update user and search users . I will leave this to you as a challenge, have fun :) .

Here's a link to the full code on github.

Wow, you got this far!! You are super awesome. As always, I would like to see your contributions down in the comments.

Thanks for taking your time out to read all through :), Adios!

Top comments (0)