MERN stack is a free JavaScript software stack for building dynamic web applications, consisting of four key technologies.
- MongoDB
- ExpressJS
- ReactJS
- NodeJS
This blog is the second part of a ‘Getting started with MERN stack’ series that aims to get you acquainted with the MERN stack. Read the first one here, before moving on with this one.
Connecting to MongoDB Atlas
Follow the Get Started with Atlas guide to create an account, deploy your first cluster, and locate your cluster’s connection string.
Now that you have the connection string, go back to the ‘backend’ directory and create a ‘.env’ file under config directory. There, assign the connection string to a new MONGO_URI variable. Once done, your file should look similar to the one below. Replace
NODE_ENV = development
PORT = 5000
MONGO_URI = mongodb+srv://<username>:<password>@mahatcluster.8vj8e.mongodb.net/mernapp?retryWrites=true&w=majority
We can add the following code to connect to our database under the config folder and inside the db.js file.
const mongoose = require('mongoose')
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI)
console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline)
} catch {
console.log(error)
process.exit(1)
}
}
module.exports = connectDB
After that we bring the connectDB() to server.js
const connectDB = require('./config/db')
connectDB()
Building the RESTful APIs with the MERN stack
Create a folder named routes. In it, create a file named userRoutes.js which will forward the route to a particular API inside userController.js under the controller folder.
Also, apply middleware to protect the user route and to change the default express error handler. Here, ‘protect’ middleware function is used to protect the route.
// userRoutes.js
const express = require('express')
const router = express.Router()
const {registerUser, loginUser, getMe} = require('../controllers/userController')
const {protect} = require('../middleware/authMiddleware')
router.post('/', registerUser)
router.post('/login', loginUser)
router.get('/me', protect, getMe)
module.exports = router
In the above code, I’ve created three different routes, one for creating/registering users, another for logging in or authenticating user, and the last one for getting user details. Two of them are post requests and one is a get request.
You might also be interested in reading: Getting Started with React Native
Now, we need to install express-async-handler and jsonwebtoken
npm i express-async-handler jsonwebtoken bcryptjs
Write API logic inside userController.js under the controller folder and we bring the user model here which will be discussed later. We need to bring jsonwebtoken for representing claims securely between two parties and bcryptjs to hash passwords.
// userController.js
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const asyncHandler = require('express-async-handler')
const User = require('../models/userModel')
// @desc Register new user
// @route POST /api/users
// @access Public
const registerUser = asyncHandler(async (req, res) => {
const { name, email, password } = req.body
if(!name || !email || !password) {
res.status(400)
throw new Error('Please add all fields')
}
// Check if user exists
const userExists = await User.findOne({email})
if(userExists) {
res.status(400)
throw new Error('User already exists')
}
// Hash password
const salt = await bcrypt.genSalt(10)
const hashedPassword = await bcrypt.hash(password, salt)
// Create user
const user = await User.create({
name,
email,
password: hashedPassword
})
if(user) {
res.status(201).json({
_id: user.id,
name: user.name,
email: user.email,
token: generateToken(user._id)
})
} else {
res.status(400)
throw new Error('Invalid user data')
}
})
// @desc Authenticate a user
// @route POST /api/users/login
// @access Public
const loginUser = asyncHandler(async (req, res) => {
const {email, password} = req.body
// Check for user email
const user = await User.findOne({email})
if(user && (await bcrypt.compare(password, user.password))) {
res.json({
_id: user.id,
name: user.name,
email: user.email,
token: generateToken(user._id)
})
} else {
res.status(400)
throw new Error('Invalid credentials')
}
})
// @desc Get user data
// @route GET /api/users/me
// @access Private
const getMe = asyncHandler(async (req, res) => {
res.status(200).json(req.user)
})
// Generate JWT
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: '30d',
})
}
module.exports = {
registerUser,
loginUser,
getMe,
}
We are using express-async-handler for handling exceptions inside of async express routes and passing them to our express error handlers.
Also, we are going to check to see if the created API is working properly using postman.
Under the folder backend, create another folder named middleware and two files called authMiddleware.js and errorMiddleware.js inside middleware.
// authMiddeware.js
const jwt = require('jsonwebtoken')
const asyncHandler = require('express-async-handler')
const User = require('../models/userModel')
const protect = asyncHandler(async (req, res, next) => {
let token
if(req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
try {
// Get token from header
token = req.headers.authorization.split(' ')[1]
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET)
// Get user from the token
req.user = await User.findById(decoded.id).select('-password')
next()
} catch (error) {
console.log(error)
res.status(401)
throw new Error('Not authorized')
}
}
if(!token) {
res.status(401)
throw new Error('Not authorized')
}
})
module.exports = { protect }
In the authMiddleware.js, I’m trying to get a token from request headers, verify it with JWT_SECRET, get the user(not password) from the token, and then call the next() middleware.
// errorMiddleware.js
const errorHandler = (err, req, res, next) => {
const statusCode = res.statusCode ? res.statusCode : 500
res.status(statusCode)
res.json({
message: err.message,
stack: process.env.NODE_ENV === 'production' ? null : err.stack
})
}
module.exports = {
errorHandler,
}
In the above code, we are passing the err object to overwrite the express error handler message where the status code is checked and set to status code from userController or status code set to 500. We will show error messages accordingly and stack in development mode (in production set to null).
In order to interact with our database, we need to create a model for each of our resources. So, create a folder called model inside the backend folder, and inside the model folder, create a file called userModel.js and update it with this:
const mongoose = require('mongoose')
const userSchema = mongoose.Schema(
{
name: {
type: String,
required: [true, 'Please add a name']
},
email: {
type: String,
required: [true, 'Please add an email'],
unique: true
},
password: {
type: String,
required: [true, 'Please add a password']
},
},
{
timestamps: true
}
)
module.exports = mongoose.model('User', userSchema)
We are creating a user schema passing object with fields name, email, password, and timestamps. Email must be a unique field and all fields are mandatory.
In our server.js file we need to set up route for userRoutes.js
app.use('/api/users', require('./routes/userRoutes'))
Conclusion
In the part II of the blog, we learnt how to connect to MongoDB Atlas. In the next, and final part of the series, we will learn to set up react Application.
ReactJS is a widely used JavaScript library for building interactive user interfaces. Read about our collaboration with Almond to build a mobile commerce app based on React that enables communities in developing countries to grow revenue.
Stuck with web application development? Contact us for a consultation on Application Development.