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 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 single article will be very long and boring.

So in current article, I am covering the REST API part of this project. As the title says, the REST API server is going to be build using Express and MongoDB.

So let’s begin…

1. Create new project

npx create-nuxt-app nuxt-with-express

And choose below options when asked:

Choose UI framework : Bootstrap Vue (we will use this in 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 feature 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 to. This allows us to register additional routes (typically /api routes) without need for an external server.

In general, we can have both backend and frontend on 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 “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 Controller for every Models

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:

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

npm run dev

[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

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 sourcecode 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 thing there is some improvements I should do into the code, also please leave your suggestions.

Watch Demo

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