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)