Quiz App using MERN Stack
Last Updated :
04 Mar, 2024
In this article, we’ll walk through the step-by-step process of creating a complete quiz application with MongoDB, ReactJS, ExpressJS, and NodeJS. This application will provide users with a user-friendly interface for taking and submitting quizzes and a scoreboard to check their standing among others.
Prerequisites:
Approach to Create a Quiz App with MERN Stack:
Backend:
- Set up a new NodeJS project with npm or yarn and initialize a Git repository.
- Use dotenv to manage environment variables. Set up MongoDB Atlas for cloud hosting. Configure environment variables for MongoDB connection URI and application port. Install Express, Mongoose, and cors.
- Create a new express application and set up middleware. Define API routes for handling data requests. Implement CRUD operations for questions and results data.
- Define MongoDB schemas for Question and Result models. Create Mongoose models to interact with the MongoDB database.
- Implement controller functions for handling business logic. Interact with the MongoDB database using the Mongoose models and return responses to clients.
- Configure the application to listen on the defined port. Start the server. Use nodemon for automatic server restarts during development.
React Frontend:
- Set up a new React project with create-react-app. Initialize a Git repository. Define the project structure.
- Install react-router-dom for client-side routing. Set up routes for pages/component
- Install redux, react-redux, and redux-thunk. Define actions, reducers, and the store for state management.
- Use Axios or Fetch API to fetch data from backend endpoints. Implement logic for loading, error, and success states during data fetching.
- Define UI components for rendering the quiz questions, results, and other elements. Use Bootstrap or CSS frameworks for styling.
- Implement functionalities like starting the quiz, answering questions, navigating, and displaying results. Handle user input, form submission, and button clicks.
Steps to Create the Backend Server:
Step 1: Create a directory for the project.
mkdir server
cd server
Step 2: Initialized the Express app and installing the required packages
npm init -y
Step 3: Install the necessary package in your server using the following command.
npm i express mongoose cors dotenv mongoose morgan nodemon
Project Structure:
Backend Folder Structure
The updated dependencies in package.json file of backend will look like:
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.2",
"mongoose": "^8.2.0",
"morgan": "^1.10.0",
"nodemon": "^3.1.0"
}
Example: Create the required files and write the following code.
Javascript
import express from 'express' ;
import morgan from 'morgan' ;
import cors from 'cors' ;
import { config } from 'dotenv' ;
import router from './router/route.js' ;
import connect from './database/conn.js' ;
config();
const app = express();
app.use(morgan( 'tiny' ));
app.use(cors());
app.use(express.json());
const port = process.env.PORT || 8080;
app.use( '/api' , router);
app.get( '/' , (req, res) => {
try {
res.json( "Get Request" )
} catch (error) {
res.json(error)
}
});
connect().then(() => {
app.listen(port, () => {
console.log(`Server connected to http:
});
}). catch (error => {
console.log( "Invalid Database Connection" );
});
|
Javascript
import Questions from "../models/questionSchema.js" ;
import Results from "../models/resultSchema.js" ;
import questions, { answers } from '../database/data.js'
export async function getQuestions(req, res) {
try {
let q = await Questions.find();
if (q.length === 0) {
await Questions.insertMany({ questions, answers });
q = await Questions.find();
}
res.json(q);
} catch (error) {
res.json({ error });
}
}
export async function insertQuestions(req, res){
try {
Questions.insertMany({ questions: questions, answers: answers })
} catch (error) {
res.json({ error })
}
}
export async function dropQuestions(req, res){
try {
await Questions.deleteMany();
res.json({ msg: "Questions Deleted Successfully...!" });
} catch (error) {
res.json({ error })
}
}
export async function getResult(req, res){
try {
const r = await Results.find();
res.json(r)
} catch (error) {
res.json({ error })
}
}
export async function storeResult(req, res) {
try {
const { username, result, attempts, points, achived } = req.body;
if (!username && !result) throw new Error( 'Data Not Provided...!' );
const newResult = await Results.create({ username, result, attempts, points, achived });
res.json({ msg: "Result Saved Successfully...!" , result: newResult });
} catch (error) {
res.json({ error });
}
}
export async function dropResult(req, res){
try {
await Results.deleteMany();
res.json({ msg : "Result Deleted Successfully...!" })
} catch (error) {
res.json({ error })
}
}
|
Javascript
import mongoose from "mongoose" ;
export default async function connect(){
await mongoose.connect(process.env.ATLAS_URI)
console.log( "Database Connected" )
}
|
Javascript
export default [
{
id: 1,
question : "Javascript is an _______ language" ,
options : [
'Object-Oriented' ,
'Object-Based' ,
'Procedural' ,
]
},
{
id: 2,
question : "Following methods can be used to display data in some form using Javascript" ,
options : [
'document.write()' ,
'console.log()' ,
'window.alert()' ,
]
},
{
id: 3,
question : "When an operator value is NULL, the typeof returned by the unary operator is:" ,
options : [
'Boolean' ,
'Undefined' ,
'Object' ,
]
},
{
id: 4,
question : "What does the toString() method return?" ,
options : [
'Return Object' ,
'Return String' ,
'Return Integer'
]
},
{
id: 5,
question : "Which function is used to serialize an object into a JSON string?" ,
options : [
'stringify()' ,
'parse()' ,
'convert()' ,
]
}
];
export const answers = [0, 1, 2, 1, 0];
|
Javascript
import mongoose from "mongoose" ;
const { Schema } = mongoose;
const questionModel = new Schema({
questions: { type : Array, default : []},
answers : { type : Array, default : []},
createdAt: { type: Date, default : Date.now },
});
export default mongoose.model( 'Question' , questionModel);
|
Javascript
import mongoose from "mongoose" ;
const { Schema } = mongoose;
const resultModel = new Schema({
username : { type : String },
result : { type : Array, default : []},
attempts : { type : Number, default : 0},
points : { type : Number, default : 0},
achived : { type : String, default : '' },
createdAt : { type : Date, default : Date.now}
})
export default mongoose.model( 'result' , resultModel);
|
Javascript
import { Router } from "express" ;
import * as controller from '../controllers/controller.js' ;
const router = Router();
router.route( '/' )
router.route( '/questions' )
.get(controller.getQuestions)
.post(controller.insertQuestions)
. delete (controller.dropQuestions);
router.route( '/result' )
.get(controller.getResult)
.post(controller.storeResult)
. delete (controller.dropResult);
export default router;
|
Steps to Setup Frontend with React
Step 1: Create React App
npx create-react-app client
Step 2: Switch to the project directory
cd client
Step 3: Installing the required packages:
npm install react-redux react-router-dom axios nodemon @reduxjs/toolkit
Step 4: Create a folder inside the src folder i.e. components, helper, hooks, and redux. Inside component create App.js, Main.js, Question.js, Quiz.js, Result.js, ResultTable.js. Inside helper create helper.js, in the hooks folder create FetchQuestion.js, setResult.js and inside the redux folder create question_reducer.js, result_reducer.js, store.js.
Project Structure:
Frontend Folder Structure
The updated dependency in package.json file of frontend will look like:
"dependencies": {
"@reduxjs/toolkit": "^2.2.1",
"axios": "^1.6.7",
"nodemon": "^3.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-redux": "^9.1.0",
"react-router-dom": "^6.22.1",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
Example: Create the required files and write the following code.
CSS
:root{
--primary- color : #0DFF92 ;
--dark- color : #222222 ;
--light- color : #f0f0f0 ;
}
body, html{
height : 100% ;
background : var(--dark-color)
}
* > *{
font-family : 'Poppins' , sans-serif ;
}
.container{
display : block ;
position : relative ;
margin : 40px auto ;
height : auto ;
width : 800px ;
padding : 20px ;
}
.container .title{
font-size : 3em ;
text-align : center ;
border : 5px solid var(--primary-color);
padding : . 3em . 2em ;
border-radius: 4px ;
}
.text-light {
color : var(--light-color)
}
.container ul{
list-style : none ;
margin : 0 ;
padding : 0 ;
overflow : auto ;
}
.container .questions{
padding : 3em ;
}
.container .grid{
margin-top : 3em ;
display : grid;
grid-template-columns: 1 fr 1 fr;
}
.container .btn{
padding : . 2em 1.7em ;
border : none ;
border-radius: . 1em ;
font-size : 1.2em ;
}
.container .btn:hover{
cursor : pointer ;
background-color : #f0f0f0 ;
color : #202020 ;
}
.next{
background-color : var(--primary-color);
justify-self: flex-end;
}
.prev{
background-color : #faff5a ;
justify-self: flex-start;
}
ul li{
color : #AAAAAA ;
display : block ;
position : relative ;
float : left ;
width : 100% ;
height : 100px ;
border-bottom : 1px solid #333 ;
}
ul li input[type=radio]{
position : absolute ;
visibility : hidden ;
}
ul li label{
display : block ;
position : relative ;
font-weight : 300 ;
font-size : 1.35em ;
padding : 25px 25px 25px 80px ;
margin : 10px auto ;
height : 30px ;
z-index : 9 ;
cursor : pointer ;
-webkit-transition: all 0.25 s linear;
}
ul li:hover label{
color : #FFFFFF ;
}
ul li .check{
display : block ;
position : absolute ;
border : 5px solid #AAAAAA ;
border-radius: 100% ;
height : 25px ;
width : 25px ;
top : 30px ;
left : 20px ;
z-index : 5 ;
transition: border . 25 s linear;
-webkit-transition: border . 25 s linear;
}
ul li:hover .checked {
border : 5px solid #FFFFFF ;
}
ul li .check::before {
display : block ;
position : absolute ;
content : '' ;
border-radius: 100% ;
height : 15px ;
width : 15px ;
top : 5px ;
left : 5px ;
margin : auto ;
transition: background 0.25 s linear;
-webkit-transition: background 0.25 s linear;
}
input[type=radio]:checked ~ .check {
border : 5px solid var(--primary-color)
}
input[type=radio]:checked ~ .check::before{
background : var(--primary-color)
}
input[type=radio]:checked ~ .text-primary{
color : var(--primary-color)
}
.checked {
border : 5px solid var(--primary-color) !important ;
}
.checked::before{
background : var(--primary-color)
}
|
CSS
body {
margin : 0 ;
font-family : -apple-system, BlinkMacSystemFont, 'Segoe UI' , 'Roboto' , 'Oxygen' ,
'Ubuntu' , 'Cantarell' , 'Fira Sans' , 'Droid Sans' , 'Helvetica Neue' ,
sans-serif ;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family : source-code-pro, Menlo, Monaco, Consolas, 'Courier New' ,
monospace ;
}
|
CSS
.container ol li {
font-size : 1.4em ;
color : #cecece ;
}
.start {
display : flex;
justify- content : center ;
padding-top : 2em ;
}
.start .btn {
padding : . 2em 1.7em ;
border : none ;
border-radius: . 1em ;
font-size : 1.2em ;
color : #202020 ;
text-decoration : none ;
background-color : #faff5a ;
}
#form {
display : flex;
justify- content : center ;
margin-top : 4em ;
}
#form .userid {
padding : . 7em 2em ;
width : 50% ;
border : none ;
border-radius: 3px ;
font-size : 1em ;
}
|
CSS
.flex- center {
display : flex;
justify- content : center ;
flex- direction : column;
border : 1px solid #cecece ;
padding : 3em 4em ;
gap: 1em ;
}
.container .flex {
display : flex;
justify- content : space-between;
}
.container .flex span {
font-size : 1.4em ;
color : #cecece ;
}
.container .flex span.achive {
font-weight : bold ;
color : #ff2a66 ;
color : #2aff95 ;
}
table {
width : 100% ;
}
.table-header {
color : #cecece ;
font-size : 1.1em ;
text-align : center ;
background : #212121 ;
padding : 18px 0 ;
}
.table-body {
font-size : 1.1em ;
text-align : center ;
background : #d8d8d8 ;
padding : 18px 0 ;
}
.table-header>tr>td {
border : 1px solid #faff5a ;
}
|
Javascript
import React from 'react' ;
import ReactDOM from 'react-dom/client' ;
import './styles/index.css' ;
import App from './components/App' ;
import store from './redux/store' ;
import { Provider } from 'react-redux' ;
const root = ReactDOM.createRoot(document.getElementById( 'root' ));
root.render(
<Provider store={store}>
<App />
</Provider>
);
|
Javascript
import React, { useEffect } from 'react'
import '../styles/Result.css' ;
import { Link } from 'react-router-dom' ;
import ResultTable from './ResultTable' ;
import {
useDispatch,
useSelector
} from 'react-redux' ;
import {
attempts_Number,
earnPoints_Number,
flagResult
} from '../helper/helper' ;
import {
resetAllAction
} from '../redux/question_reducer' ;
import {
resetResultAction
} from '../redux/result_reducer' ;
import {
usePublishResult
} from '../hooks/setResult' ;
export default function Result() {
const dispatch = useDispatch()
const { questions: { queue, answers }, result:
{ result, userId } } = useSelector(state => state)
const totalPoints = queue.length * 10;
const attempts = attempts_Number(result);
const earnPoints = earnPoints_Number(result, answers, 10)
const flag = flagResult(totalPoints, earnPoints)
usePublishResult({
result,
username: userId,
attempts,
points: earnPoints,
achived: flag ? "Passed" : "Failed"
});
function onRestart() {
dispatch(resetAllAction())
dispatch(resetResultAction())
}
return (
<div className= 'container' >
<h1 className= 'title text-light' >Quiz Application</h1>
<div className= 'result flex-center' >
<div className= 'flex' >
<span>Username</span>
<span className= 'bold' >{userId || "" }</span>
</div>
<div className= 'flex' >
<span>Total Quiz Points : </span>
<span className= 'bold' >{totalPoints || 0}</span>
</div>
<div className= 'flex' >
<span>Total Questions : </span>
<span className= 'bold' >{queue.length || 0}</span>
</div>
<div className= 'flex' >
<span>Total Attempts : </span>
<span className= 'bold' >{attempts || 0}</span>
</div>
<div className= 'flex' >
<span>Total Earn Points : </span>
<span className= 'bold' >{earnPoints || 0}</span>
</div>
<div className= 'flex' >
<span>Quiz Result</span>
<span style={{ color: `${flag ? "#2aff95" : "#ff2a66" }` }}
className= 'bold' >{flag ? "Passed" : "Failed" }</span>
</div>
</div>
<div className= "start" >
<Link className= 'btn' to={ '/' }
onClick={onRestart}>Restart</Link>
</div>
<div className= "container" >
{ }
<ResultTable></ResultTable>
</div>
</div>
)
}
|
Javascript
import React, { useEffect, useState } from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { useFetchQestion } from '../hooks/FetchQuestion'
import { updateResult } from '../hooks/setResult'
export default function Questions({ onChecked }) {
const [checked, setChecked] = useState(undefined)
const { trace } = useSelector(state => state.questions);
const result = useSelector(state => state.result.result);
const [{ isLoading, apiData, serverError }] = useFetchQestion()
const questions = useSelector(state =>
state.questions.queue[state.questions.trace])
const dispatch = useDispatch()
useEffect(() => {
dispatch(updateResult({ trace, checked }))
}, [checked])
function onSelect(i) {
onChecked(i)
setChecked(i)
dispatch(updateResult({ trace, checked }))
}
if (isLoading) return <h3 className= 'text-light' >isLoading</h3>
if (serverError) return
<h3 className= 'text-light' >
{serverError || "Unknown Error" }
</h3>
return (
<div className= 'questions' >
<h2 className= 'text-light' >{questions?.question}</h2>
<ul key={questions?.id}>
{
questions?.options.map((q, i) => (
<li key={i}>
<input
type= "radio"
value={ false }
name= "options"
id={`q${i}-option`}
onChange={() => onSelect(i)}
/>
<label className= 'text-primary'
htmlFor={`q${i}-option`}>{q}</label>
<div className={
`check ${result[trace] == i ? 'checked' : '' }`}>
</div>
</li>
))
}
</ul>
</div>
)
}
|
Javascript
import React, { useEffect, useState } from 'react'
import { getServerData } from '../helper/helper'
export default function ResultTable() {
const [data, setData] = useState([])
useEffect(() => {
getServerData(`
${process.env.REACT_APP_SERVER_HOSTNAME}/api/result`, (res) => {
setData(res)
})
})
return (
<div>
<table>
<thead className= 'table-header' >
<tr className= 'table-row' >
<td>Name</td>
<td>Attemps</td>
<td>Earn Points</td>
<td>Result</td>
</tr>
</thead>
<tbody>
{!data ?? <div>No Data Found </div>}
{
data.map((v, i) => (
<tr className= 'table-body' key={i}>
<td>{v?.username || '' }</td>
<td>{v?.attempts || 0}</td>
<td>{v?.points || 0}</td>
<td>{v?.achived || "" }</td>
</tr>
))
}
</tbody>
</table>
</div>
)
}
|
Javascript
import { useSelector } from "react-redux" ;
import { Navigate } from "react-router-dom" ;
import axios from 'axios'
export function attempts_Number(result) {
return result.filter(r => r !== undefined).length;
}
export function earnPoints_Number(result, answers, point) {
return result.map((element, i) =>
answers[i] === element).filter(i => i).map(i => point).reduce(
(prev, curr) => prev + curr, 0);
}
export function flagResult(totalPoints, earnPoints) {
return (totalPoints * 50 / 100) < earnPoints;
}
export function CheckUserExist({ children }) {
const auth = useSelector(state => state.result.userId)
return auth ? children :
<Navigate to={ '/' } replace={ true }></Navigate>
}
export async function getServerData(url, callback) {
const data = await (await axios.get(url))?.data;
return callback ? callback(data) : data;
}
export async function postServerData(url, result, callback) {
const data = await (await axios.post(url, result))?.data;
return callback ? callback(data) : data;
}
|
Javascript
import { useEffect, useState } from "react"
import { useDispatch } from "react-redux" ;
import { getServerData } from "../helper/helper" ;
import * as Action from '../redux/question_reducer'
export const useFetchQestion = () => {
const dispatch = useDispatch();
const [getData, setGetData] = useState({
isLoading: false ,
apiData: [], serverError: null
});
useEffect(() => {
setGetData(prev => ({ ...prev, isLoading: true }));
(async () => {
try {
const [{ questions, answers }] = await getServerData(`
${process.env.REACT_APP_SERVER_HOSTNAME}/api/questions`,
(data) => data)
if (questions.length > 0) {
setGetData(prev => ({ ...prev, isLoading: false }));
setGetData(prev => ({ ...prev, apiData: questions }));
dispatch(Action.startExamAction({
question: questions, answers
}))
} else {
throw new Error( "No Question Avalibale" );
}
} catch (error) {
setGetData(prev => ({ ...prev, isLoading: false }));
setGetData(prev => ({ ...prev, serverError: error }));
}
})();
}, [dispatch]);
return [getData, setGetData];
}
export const MoveNextQuestion = () => async (dispatch) => {
try {
dispatch(Action.moveNextAction());
} catch (error) {
console.log(error)
}
}
export const MovePrevQuestion = () => async (dispatch) => {
try {
dispatch(Action.movePrevAction());
} catch (error) {
console.log(error)
}
}
|
Javascript
import { postServerData } from '../helper/helper'
import * as Action from '../redux/result_reducer'
export const PushAnswer = (result) => async (dispatch) => {
try {
await dispatch(Action.pushResultAction(result))
} catch (error) {
console.log(error)
}
}
export const updateResult = (index) => async (dispatch) => {
try {
dispatch(Action.updateResultAction(index));
} catch (error) {
console.log(error)
}
}
export const usePublishResult = (resultData) => {
const { result, username } = resultData;
(async () => {
try {
if (result !== [] && !username)
throw new Error( "Couldn't get Result" );
await postServerData(`http:
resultData, data => data)
} catch (error) {
console.log(error)
}
})();
}
|
Javascript
import { createSlice } from "@reduxjs/toolkit" ;
export const questionReducer = createSlice({
name: 'questions' ,
initialState: {
queue: [],
answers: [],
trace: 0
},
reducers: {
startExamAction: (state, action) => {
let { question, answers } = action.payload
return {
...state,
queue: question,
answers
}
},
moveNextAction: (state) => {
return {
...state,
trace: state.trace + 1
}
},
movePrevAction: (state) => {
return {
...state,
trace: state.trace - 1
}
},
resetAllAction: () => {
return {
queue: [],
answers: [],
trace: 0
}
}
}
})
export const { startExamAction,
moveNextAction,
movePrevAction,
resetAllAction } = questionReducer.actions;
export default questionReducer.reducer;
|
Javascript
import { createSlice } from "@reduxjs/toolkit"
export const resultReducer = createSlice({
name: 'result' ,
initialState: {
userId: null ,
result: []
},
reducers: {
setUserId: (state, action) => {
state.userId = action.payload
},
pushResultAction: (state, action) => {
state.result.push(action.payload)
},
updateResultAction: (state, action) => {
const { trace, checked } = action.payload;
state.result.fill(checked, trace, trace + 1)
},
resetResultAction: () => {
return {
userId: null ,
result: []
}
}
}
})
export const { setUserId,
pushResultAction,
resetResultAction,
updateResultAction } = resultReducer.actions;
export default resultReducer.reducer;
|
Javascript
import {
combineReducers,
configureStore
} from '@reduxjs/toolkit' ;
import questionReducer from './question_reducer' ;
import resultReducer from './result_reducer' ;
const rootReducer = combineReducers({
questions: questionReducer,
result: resultReducer
})
export default configureStore({ reducer: rootReducer });
|
Javascript
import './App.css' ;
import {
createBrowserRouter,
RouterProvider
} from 'react-router-dom'
import Main from './Main.js' ;
import Quiz from './Quiz.js' ;
import Result from './Result.js' ;
import { CheckUserExist } from '../src/helper/' ;
const router = createBrowserRouter([
{
path: '/' ,
element: <Main></Main>
},
{
path: '/quiz' ,
element: <CheckUserExist><Quiz /></CheckUserExist>
},
{
path: '/result' ,
element: <CheckUserExist><Result /></CheckUserExist>
},
])
function App() {
return (
<>
<RouterProvider router={router} />
</>
);
}
export default App;
|
Steps to run the App:
node server.js
npm start
Output:
Output
Share your thoughts in the comments
Please Login to comment...