Aslam Doctor

Simple CRUD App using Express & Nuxt JS using serverMiddleware – Part 1/2

Simple CRUD App using Express & Nuxt JS using serverMiddleware – Part 1/2

So recently I was trying to implement a mini-project with the help of NuxtJS and Express as API server and got to know that NuxtJS comes with an awesome feature called “serverMiddleware”. It allows us to run both backend and frontend from the same server. This is very helpful for creating API endpoints. So I tried to implement a basic CRUD project and it worked really well. In fact, I am planning to implement some of my future in-house projects using this stack.

I am going to divide this tutorial into 2 parts as putting all the codes and explaining them in a single article will be very long and boring.
So in the current article, I am covering the REST API part of this project. As the title says, the REST API server is going to be built using Express and MongoDB.

So let’s begin…

1. Create a new project

npx create-nuxt-app nuxt-with-express

And choose the below options when asked:

Choose UI framework: Bootstrap Vue (we will use this in the next part of this article)

Choose custom server framework: Express

Choose NuxtJS modules: axios

Use other options as per your needs.

2. Install required packages

cd nuxt-with-express
npm install express-validator jsonwebtoken mongoose bcryptjs

Explanation:

express-validator : to validate data submitted by forms
jsonwebtoken : to generate and verify login token
mongoose : schema-based solution to interact with MongoDB database
bcryptjs : to encode/decode authentication token generated using jsonwebtoken

3. Nuxt Configurations

In this setup, we are going to use one of the most powerful features of NuxtJS which is “serverMiddleware”.
As it says in the documentation: Nuxt internally creates a connect instance that we can add our own custom middleware. This allows us to register additional routes (typically /API routes) without the need for an external server.

In general, we can have both backend and frontend on the same server. To enable serverMiddleware, add these lines at the end of the page before closing curly brackets “}”.

serverMiddleware: [
    '~/api/index.js'
]

This tells NuxtJS that we have our API server running from /api directory using the “index.js” file.

4. Start implementing the API folder

Folder Structure:

/api
–> /controllers – all the business logic for all individual modules
–> /models – create MongoDB schema and define them in this folder in separate files for every schema
–> /routes – create API routes for every models created inside /models folder
–> db.js – all the DB connection related stuff will be in this file
–> index.js – main file to run the API server
–> config.js – to store and access all global variables & functions. e.g. authentication token, checkAuthenticated() etc.

In some steps I won’t be explaining every line of code as the comments in the code already explains it.

/api/db.js

const mongoose = require('mongoose');
// mongodb database connection string. change it as per your needs. here "mydb" is the name of the database. You don't need to create DB from mongodb terminal. mongoose create the db automatically.
mongoose.connect('mongodb://localhost/mydb', {
                            useNewUrlParser: true,
                            useUnifiedTopology: true,
                            useFindAndModify: false,
                            useCreateIndex: true
                          });
var db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function callback () {
  console.log("MongoDB Connected...");
});

module.exports = db

/api/config.js

const jwt = require('jsonwebtoken');

const config = {
  authSecret:'mysecret', // secret for generating jwt token
}

module.exports = config

// check if user logged in
module.exports.isAuthenticated = function (req, res, next) {
  var token = req.headers.authorization
  if (token) {
    // verifies secret and checks if the token is expired
    jwt.verify(token.replace(/^Bearer\s/, ''), config.authSecret, function(err, decoded) {
      if (err) {
        return res.status(401).json({message: 'unauthorized'})
      } else {
        return next();
      }
    });
  }
  else{
    return res.status(401).json({message: 'unauthorized'})
  }
}

/api/index.js

const express = require('express')
const db = require('./db')


// Create express instnace
const app = express()

// Init body-parser options (inbuilt with express)
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Require & Import API routes
const users = require('./routes/users')
const articles = require('./routes/articles')

// Use API Routes
app.use(users)
app.use(articles)

// Export the server middleware
module.exports = {
  path: '/api',
  handler: app
}

5. Create Models Schema

In this step, we define MongoDB schemas. The mongoose package reads this code and created the schema automatically inside MongoDB. So you don’t need to create any schema by logging into MongoDB manually.

/api/models/Article.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const Article = new Schema ({
  title: { type: String, required: true, index: { unique: true } },
  author: { type: String, required: true },
  body: { type: String, required: true },
});

module.exports = mongoose.model('Article', Article)

/api/models/User.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const User = new Schema ({
  full_name: { type: String, required: true },
  email: { type: String, required: true, index: { unique: true } },
  password: { type: String, required: true },
});

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

6. Create a Controller for every Model

In this step, we will create business logic for every Model by creating their controller files.
Also, these controllers will be used to create API routes.

/api/controllers/usersController.js

const config = require('../config')
const User = require('../models/User')
const validator = require('express-validator')
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs')


// Register
module.exports.register = [
  // validations rules
  validator.body('full_name', 'Please enter Full Name').isLength({ min: 1 }),
  validator.body('email', 'Please enter Email').isLength({ min: 1 }),
  validator.body('email').custom(value => {
    return User.findOne({email:value}).then(user => {
      if (user !== null) {
        return Promise.reject('Email already in use');
      }
    })
  }),
  validator.body('password', 'Please enter Password').isLength({ min: 1 }),

  function(req, res) {
    // throw validation errors
    const errors = validator.validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.mapped() });
    }

    // initialize record
    var user = new User({
        full_name : req.body.full_name,
        email : req.body.email,
        password : req.body.password,
    })

    // encrypt password
    var salt = bcrypt.genSaltSync(10);
    var hash = bcrypt.hashSync(user.password, salt);
    user.password = hash

    // save record
    user.save(function(err, user){
        if(err) {
            return res.status(500).json({
                message: 'Error saving record',
                error: err
            });
        }
        return res.json({
            message: 'saved',
            _id: user._id
        });
    })
  }
]


// Login
module.exports.login = [
  // validation rules
  validator.body('email', 'Please enter Email').isLength({ min: 1 }),
  validator.body('password', 'Please enter Password').isLength({ min: 1 }),

  function(req, res) {
    // throw validation errors
    const errors = validator.validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.mapped() });
    }

    // validate email and password are correct
    User.findOne({email: req.body.email}, function(err, user){
        if(err) {
            return res.status(500).json({
                message: 'Error logging in',
                error: err
            });
        }

        if (user === null) {
          return res.status(500).json({
            message: 'Email address you entered is not found.'
          });
        }

        // compare submitted password with password inside db
        return bcrypt.compare(req.body.password, user.password, function(err, isMatched) {
          if(isMatched===true){
            return res.json({
              user: {
                _id: user._id,
                email: user.email,
                full_name: user.full_name
              },
              token: jwt.sign({_id: user._id, email: user.email, full_name: user.full_name}, config.authSecret) // generate JWT token here
            });
          }
          else{
            return res.status(500).json({
              message: 'Invalid Email or Password entered.'
            });
          }
        });
    });
  }
]

// Get User
module.exports.user = function(req, res) {
  var token = req.headers.authorization
  if (token) {
    // verifies secret and checks if the token is expired
    jwt.verify(token.replace(/^Bearer\s/, ''), config.authSecret, function(err, decoded) {
      if (err) {
        return res.status(401).json({message: 'unauthorized'})
      } else {
        return res.json({ user: decoded })
      }
    });
  }
  else{
    return res.status(401).json({message: 'unauthorized'})
  }
}

/api/controllers/articlesController.js

const Article = require('../models/Article');
const validator = require('express-validator');

// Get all
module.exports.list = function (req, res, next) {
  Article.find({}, function(err, articles){
    if(err) {
        return res.status(500).json({
            message: 'Error getting records.'
        });
    }
    return res.json(articles);
  });
}


// Get one
module.exports.show = function(req, res) {
  var id = req.params.id;
  Article.findOne({_id: id}, function(err, article){
      if(err) {
          return res.status(500).json({
              message: 'Error getting record.'
          });
      }
      if(!article) {
          return res.status(404).json({
              message: 'No such record'
          });
      }
      return res.json(article);
  });
}


// Create
module.exports.create = [
  // validations rules
  validator.body('title', 'Please enter Article Title').isLength({ min: 1 }),
  validator.body('title').custom(value => {
    return Article.findOne({title:value}).then(article => {
      if (article !== null) {
        return Promise.reject('Title already in use');
      }
    })
  }),
  validator.body('author', 'Please enter Author Name').isLength({ min: 1 }),
  validator.body('body', 'Please enter Article Content').isLength({ min: 1 }),

  function(req, res) {
    // throw validation errors
    const errors = validator.validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.mapped() });
    }

    // initialize record
    var article = new Article({
        title : req.body.title,
        author : req.body.author,
        body : req.body.body,
    })

    // save record
    article.save(function(err, article){
        if(err) {
            return res.status(500).json({
                message: 'Error saving record',
                error: err
            });
        }
        return res.json({
            message: 'saved',
            _id: article._id
        });
    })
  }
]

// Update
module.exports.update = [
  // validation rules
  validator.body('title', 'Please enter Article Title').isLength({ min: 1 }),
  validator.body('title').custom( (value, {req}) => {
    return Article.findOne({ title:value, _id:{ $ne: req.params.id } })
      .then( article => {
      if (article !== null) {
        return Promise.reject('Title already in use');
      }
    })
  }),
  validator.body('author', 'Please enter Author Name').isLength({ min: 1 }),
  validator.body('body', 'Please enter Article Content').isLength({ min: 1 }),

  function(req, res) {
    // throw validation errors
    const errors = validator.validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(422).json({ errors: errors.mapped() });
    }

    var id = req.params.id;
    Article.findOne({_id: id}, function(err, article){
        if(err) {
            return res.status(500).json({
                message: 'Error saving record',
                error: err
            });
        }
        if(!article) {
            return res.status(404).json({
                message: 'No such record'
            });
        }

        // initialize record
        article.title =  req.body.title ? req.body.title : article.title;
        article.author =  req.body.author ? req.body.author : article.author;
        article.body =  req.body.body ? req.body.body : article.body;

        // save record
        article.save(function(err, article){
            if(err) {
                return res.status(500).json({
                    message: 'Error getting record.'
                });
            }
            if(!article) {
                return res.status(404).json({
                    message: 'No such record'
                });
            }
            return res.json(article);
        });
    });
  }

]


// Delete
module.exports.delete = function(req, res) {
  var id = req.params.id;
  Article.findByIdAndRemove(id, function(err, article){
      if(err) {
          return res.status(500).json({
              message: 'Error getting record.'
          });
      }
      return res.json(article);
  });
}

7. Create Routes for every Controller

So as mentioned in step 6, we will use the controllers to create API routes.

/api/routes/users.js

const config = require('../config')
const { Router } = require('express')

const router = Router()

// Initialize Controller
const usersController = require('../controllers/usersController')

// Register
router.post('/users/register', usersController.register)

// Login
router.post('/users/login', usersController.login)

// Get User
router.get('/users/user', usersController.user)

module.exports = router

/api/routes/articles.js

const config = require('../config')
const { Router } = require('express')

const router = Router()

// Initialize Controller
const articlesController = require('../controllers/articlesController')

// Get All
router.get('/articles', articlesController.list)

// Get One
router.get('/articles/:id', articlesController.show)

// Create
router.post('/articles', config.isAuthenticated, articlesController.create)

// Update
router.put('/articles/:id', config.isAuthenticated, articlesController.update)

// Delete
router.delete('/articles/:id', config.isAuthenticated, articlesController.delete)

module.exports = router

Notice the “config.isAuthenticated” in this code. It is a middleware used for securing specific routes.

Below is the list of all our endpoints:

[POST] /api/users/register => register user
[POST] /api/users/login => log in user
[GET] /api/users/user => get logged in user details
[GET] /api/articles => get all articles
[GET] /api/articles/:id => get single article
[POST] /api/articles => create article
[PUT] /api/articles/:id => update article
[DELETE] /api/articles/:id => delete article

8. Run the server and see if all is OK.

npm run dev

And test all the API endpoints using Postman tool. As per the endpoints, all the URLs will be like this

http://localhost:3000/api/user/register
http://localhost:3000/api/user/login
http://localhost:3000/api/user/user
http://localhost:3000/api/articles
http://localhost:3000/api/articles/:id
.. and so on. Just make sure you use proper request types in Postman.

So the REST API part ends here.

You can find the source code of this full project over here
https://github.com/aslamdoctor/nuxt-with-express

In the next article, I will be covering the Front-end implementation of this API using NuxtJS.

Hope you guys like this article. If you find any issues, please leave comments.
I have tried to keep the code as simple as possible. If you guys think there are some improvements I should do to the code, also please leave your suggestions.

Watch Demo

** Update: Part 2 of this article is up now. Click here to check it.