← Back to Blog
nodejsmern-stackexpressjsmongodbreact

Getting Started with MERN Stack

Getting Started with MERN Stack

The MERN stack is one of the most popular full-stack JavaScript frameworks. In this tutorial we'll build a full-stack web application with user authentication using MongoDB, Express, React, and Node.js.


What is MERN?

Layer Technology Purpose
Database MongoDB NoSQL document store
Backend Express + Node.js REST API server
Frontend React UI / client side

Backend Setup

1. Initialise the project

bash
mkdir mern-app && cd mern-app
npm init -y
npm install express dotenv mongoose colors bcryptjs jsonwebtoken express-async-handler
npm install -D nodemon

Add scripts to package.json:

json
{
  "scripts": {
    "start": "node backend/server.js",
    "dev": "nodemon backend/server.js"
  }
}

2. Connect to MongoDB Atlas

Create .env:

NODE_ENV=development
PORT=5000
MONGO_URI=mongodb+srv://<username>:<password>@cluster.mongodb.net/mernapp
JWT_SECRET=your_jwt_secret
javascript
// backend/config/db.js
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 (error) {
    console.log(error)
    process.exit(1)
  }
}

module.exports = connectDB

3. User Model

javascript
// backend/models/userModel.js
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)

4. Auth Controller

javascript
// backend/controllers/userController.js
const jwt = require('jsonwebtoken')
const bcrypt = require('bcryptjs')
const asyncHandler = require('express-async-handler')
const User = require('../models/userModel')

// Register user
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')
  }

  const userExists = await User.findOne({ email })
  if (userExists) {
    res.status(400)
    throw new Error('User already exists')
  }

  const salt = await bcrypt.genSalt(10)
  const hashedPassword = await bcrypt.hash(password, salt)

  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')
  }
})

// Login user
const loginUser = asyncHandler(async (req, res) => {
  const { email, password } = req.body
  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')
  }
})

const generateToken = (id) =>
  jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: '30d' })

module.exports = { registerUser, loginUser }

5. Routes & Middleware

javascript
// backend/routes/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
javascript
// backend/middleware/authMiddleware.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?.startsWith('Bearer')) {
    try {
      token = req.headers.authorization.split(' ')[1]
      const decoded = jwt.verify(token, process.env.JWT_SECRET)
      req.user = await User.findById(decoded.id).select('-password')
      next()
    } catch (error) {
      res.status(401)
      throw new Error('Not authorised')
    }
  }
  if (!token) {
    res.status(401)
    throw new Error('Not authorised, no token')
  }
})

module.exports = { protect }

Frontend Setup

1. Create React App with Redux Toolkit

bash
npx create-react-app frontend --template redux
cd frontend
npm install axios react-toastify react-router-dom

2. Auth Slice (Redux)

javascript
// frontend/src/features/auth/authSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import authService from './authService'

const user = JSON.parse(localStorage.getItem('user'))

const initialState = {
  user: user || null,
  isLoading: false,
  isError: false,
  isSuccess: false,
  message: '',
}

export const register = createAsyncThunk('auth/register', async (user, thunkAPI) => {
  try {
    return await authService.register(user)
  } catch (error) {
    const message = error.response?.data?.message || error.message
    return thunkAPI.rejectWithValue(message)
  }
})

export const login = createAsyncThunk('auth/login', async (user, thunkAPI) => {
  try {
    return await authService.login(user)
  } catch (error) {
    const message = error.response?.data?.message || error.message
    return thunkAPI.rejectWithValue(message)
  }
})

const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    reset: (state) => {
      state.isLoading = false
      state.isError = false
      state.isSuccess = false
      state.message = ''
    },
    logout: (state) => {
      localStorage.removeItem('user')
      state.user = null
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(register.pending, (state) => { state.isLoading = true })
      .addCase(register.fulfilled, (state, action) => {
        state.isLoading = false
        state.isSuccess = true
        state.user = action.payload
      })
      .addCase(register.rejected, (state, action) => {
        state.isLoading = false
        state.isError = true
        state.message = action.payload
      })
  },
})

export const { reset, logout } = authSlice.actions
export default authSlice.reducer

3. Auth Service

javascript
// frontend/src/features/auth/authService.js
import axios from 'axios'

const API_URL = '/api/users/'

const register = async (userData) => {
  const response = await axios.post(API_URL, userData)
  if (response.data) localStorage.setItem('user', JSON.stringify(response.data))
  return response.data
}

const login = async (userData) => {
  const response = await axios.post(API_URL + 'login', userData)
  if (response.data) localStorage.setItem('user', JSON.stringify(response.data))
  return response.data
}

const authService = { register, login }
export default authService

Run Both Together

Install concurrently at the root level:

bash
npm install -D concurrently
json
{
  "scripts": {
    "dev": "concurrently \"npm run server\" \"npm run client\"",
    "server": "nodemon backend/server.js",
    "client": "npm start --prefix frontend"
  }
}
bash
npm run dev

Both servers start simultaneously — Express on port 5000, React on port 3000.


Summary

You now have a fully functional MERN authentication system. From here you can extend it with protected routes, user profiles, and any business logic you need. The patterns here — JWT auth, Redux state management, and RESTful APIs — are the foundation of most production MERN apps.

Share this article
← Back to all articles