nodejsmern-stackexpressjsmongodbreact
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.