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.