Open In App

Building Discussion Board In MERN Stack Course Management App

Last Updated : 23 Jul, 2025
Comments
Improve
Suggest changes
Like Article
Like
Report

A forum or discussion board is an essential feature for fostering collaboration, communication, and engagement among students in an educational platform. This tool allows students to discuss course materials, share insights, ask questions, and collaborate on projects in a structured online environment. Here’s a comprehensive overview of how to implement such a feature effectively.

Purpose and Benefits

  1. Encourage Interaction: Forums facilitate interaction among students, promoting a sense of community.
  2. Peer Learning: Students can learn from each other's perspectives and knowledge, enhancing their understanding of course content.
  3. 24/7 Accessibility: Unlike traditional classroom discussions, forums allow students to participate anytime, making it easier for those with busy schedules to engage.

Key Features

  • User Authentication: Ensure that only registered students can participate in discussions. Implement user roles (e.g., student, instructor) for different levels of access and moderation.
  • Threaded Discussions: Allow students to create topics (threads) and reply to others, creating a structured conversation flow.
  • Search Functionality: Implement a search bar for users to quickly find relevant discussions or topics.
  • Categorization: Organize discussions by subjects or courses to make navigation easier.
  • Notifications: Notify users about replies to their posts or topics they are following.
  • Moderation Tools: Provide tools for instructors to moderate discussions, such as the ability to edit or delete posts, and to manage user reports.
  • Rating and Feedback: Enable students to upvote or downvote posts, helping highlight valuable contributions.

Approach to Implement Forum or Discussion Board

Backend

  • Post and Reply Schemas: The postSchema defines the structure of posts with a title, content, author, creation date, and an array of replies. The replySchema captures individual replies with content, author, creation date, and a reference to the parent post.
  • API Endpoints: The application includes routes to get all posts, get a single post by ID, create new posts, and create replies to existing posts.
  • Creating Replies: When a reply is created, it is saved to the database, and the reply ID is added to the corresponding post's replies array to maintain a link between the post and its replies.
  • Fetching Posts and Replies: Posts can be retrieved with populated replies, allowing the frontend to display discussions along with their respective replies, enhancing the user experience.
  • Error Handling: Each API method includes error handling to manage server errors gracefully, returning appropriate responses to the client when issues occur.

Frontend

  • Post List Component: The PostList component fetches and displays a list of discussion posts from the backend, utilizing a responsive grid layout with a loading spinner during data retrieval.
  • Post Detail Component: The PostDetail component retrieves a specific post by ID, displays its title and content, and lists its associated replies, allowing users to engage in discussions.
  • Reply Submission: Users can submit replies to a post via a textarea input, which triggers an API call to save the reply and updates the post's state with the new reply upon successful submission.
  • Data Management: The application employs Axios for API requests, with error handling in place to manage potential issues when fetching posts or submitting replies.
  • UI Components: Both components utilize Chakra UI for styling, ensuring a clean and responsive design that enhances the user experience.

Backend Example:

JavaScript
// server.js

const app = require("./app")
const { config } = require("dotenv")
const database = require('./config/database');
const cloudinary = require('cloudinary').v2;

database.connectedToDatabase();

config({
    path: './config/config.env'
})

app.listen(process.env.PORT, () => {
    console.log(`Server is Running at ${process.env.PORT}`);
})
JavaScript
// app.js

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const user = require('./routes/userRoute')
const course = require('./routes/courseRoutes');
const ErrorMiddleware = require('./middleware/Error');
const cookieParser = require('cookie-parser');
const enrollment = require('./routes/enrollmentRoutes')
const quiz = require("./routes/quizRoutes")
const postRoutes = require("./routes/postRoutes");
const replyRoutes = require("./routes/replyRoutes")
const instructorRoutes = require('./routes/instructorRoutes');

const cors = require('cors')

app.use(cors({
    origin: ['http://localhost:3000'],
    methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    allowedHeaders: ['Content-Type', 'Authorization'],
    credentials: true,
}));

app.use(express.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cookieParser())

app.use('/api/v1', quiz);
app.use('/api/v1', postRoutes);
app.use('/api/v1', replyRoutes);
app.use('/api/v1', enrollment)
app.use('/api/v1', user);
app.use('/api/v1', course)

app.use('/api/v1', instructorRoutes);

app.use(ErrorMiddleware);

module.exports = app;
JavaScript
// models/posts.js

const mongoose = require("mongoose");

// Define the schema for a post
const postSchema = new mongoose.Schema({
    title : {type : String, required : true},
    content : {type : String, required : true},
    author : {
        type : String,
        required : true
    }, // This could reference a User model if implemented
    createdAt : {type : Date, default : Date.now},
    replies : [ {
        type : mongoose.Schema.Types.ObjectId,
        ref : "Reply"
    } ] // Reference to replies
});

module.exports = mongoose.model("Post", postSchema);
JavaScript
// models/reply.js

const mongoose = require('mongoose');

// Define the schema for a reply
const replySchema = new mongoose.Schema({
    content: { type: String, required: true },
    author: { type: String, required: true }, 
    createdAt: { type: Date, default: Date.now },
    post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post' }
});

module.exports = mongoose.model('Reply', replySchema);
JavaScript
// controllers/postController.js

const Post = require('../models/post');
const Reply = require('../models/reply');

// Get all posts
exports.getPosts = async (req, res) => {
    try {
        const posts = await Post.find().populate('replies');
        res.json(posts);
    } catch (error) {
        res.status(500).json({ error: 'Server error' });
    }
};

// Create a new post
exports.createPost = async (req, res) => {
    try {
        const newPost = new Post(req.body);
        console.log("ff", newPost)
        const savedPost = await newPost.save();
        res.json(savedPost);
    } catch (error) {
        res.status(500).json({ error: 'Server error' });
    }
};

// Get a post by ID
exports.getPostById = async (req, res) => {
    try {
        const post = await Post.findById(req.params.id).populate('replies');
        if (!post) {
            return res.status(404).json({ error: 'Post not found' });
        }
        res.json(post);
    } catch (error) {
        res.status(500).json({ error: 'Server error' });
    }
};
JavaScript
// controllers/replyController.js

const Post = require('../models/post');
const Reply = require('../models/reply');

// Create a reply for a post
exports.createReply = async (req, res) => {
    try {
        const { content, author } = req.body;
        const postId = req.params.postId;

        // Create a new reply
        const newReply = new Reply({
            content,
            author,
            post: postId
        });

        const savedReply = await newReply.save();

        // Add the reply to the post's replies array
        const post = await Post.findById(postId);
        post.replies.push(savedReply._id);
        await post.save();

        res.json(savedReply);
    } catch (error) {
        console.error(error);
        res.status(500).json({ error: 'Server error' });
    }
};
JavaScript
// routes/postRoutes.js

const express = require("express");
const {
    getPosts,
    createPost,
    getPostById,
} = require("../controllers/postController");

const router = express.Router();

// Get all posts
router.get("/posts", getPosts);

// Get a single post by ID
router.get("/posts/:id", getPostById);

// Create a new post
router.post("/posts", createPost);

module.exports = router;
JavaScript
// routes/replyRoutes.js

const express = require('express');
const { createReply } = require('../controllers/replyContoller');

const router = express.Router();

// Create a reply for a specific post
router.post('/:postId/replies', createReply);

module.exports = router;


Start your server using the below command:

node server.js

Frontend Example:

JavaScript
// App.js

// All Routes are follwed from previous one 
import React from 'react';
import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import Header from '../src/components/Layout/Header/Header';
import Footer from '../src/components/Layout/Footer/Footer';
import PostList from './components/Post/PostList';
import PostDetail from './components/Post/PostDetail';

function App() {
    return (
        <Router>
            <Header />
            <Routes>
                <Route path="/posts" element={<PostList />} />
                <Route path="/posts/:id" element={<PostDetail />} />
            </Routes>
            <Footer />
        </Router>
    );
}

export default App;
JavaScript
// src/components/Header/Header.jsx

import React from "react";
import { ColorModeSwitcher } from "../../../ColorModeSwitcher";
import {
    Button,
    Drawer,
    DrawerBody,
    DrawerContent,
    DrawerHeader,
    DrawerOverlay,
    HStack,
    VStack,
    useDisclosure,
} from "@chakra-ui/react";
import { RiDashboardFill, RiLogoutBoxLine, RiMenu5Fill } from "react-icons/ri";
import { Link } from "react-router-dom";

const LinkButton = ({ url = "/", title = "Home", onClose }) => (
    <Link to={url} onClick={onClose}>
        <Button variant={"ghost"}>{title}</Button>
    </Link>
);

const Header = () => {
    const { isOpen, onOpen, onClose } = useDisclosure();
    const isAuthenticated = true;
    const user = {
        role: "admin",
    };
    const logoutHandler = () => {
        console.log("Succefull");
    };
    return (
        <>
            <ColorModeSwitcher />
            <Button
                onClick={onOpen}
                colorScheme="yellow"
                width={"12"}
                height={"12"}
                rounded={"full"}
                zIndex={"overlay"}
                position={"fixed"}
                top={"6"}
                left={"6"}
            >
                <RiMenu5Fill />
            </Button>
            <Drawer placement="left" onClose={onClose} isOpen={isOpen}>
                <DrawerOverlay />
                <DrawerContent>
                    <DrawerHeader borderBottomWidth={"1px"}>BUNDLER</DrawerHeader>

                    <DrawerBody>
                        <VStack spacing={"4"} alignItems={"flex-start"}>
                            <LinkButton onClose={onClose} url="/" title="Home" />
                            <LinkButton
                                onClose={onClose}
                                url="/courses"
                                title="ALL Courses"
                            />
                            <LinkButton
                                onClose={onClose}
                                url="/quiz"
                                title="Quiz & Assessment"
                            />
                            <LinkButton
                                onClose={onClose}
                                url="request"
                                title="Request Courses"
                            />
                            <LinkButton
                                onClose={onClose}
                                url="/resource"
                                title="Resource & Material"
                            />
                            <LinkButton
                                onClose={onClose}
                                url="/posts"
                                title="Forum & Discussion"
                            />
                            <LinkButton onClose={onClose} url="/contact" title="Contact" />
                            <LinkButton onClose={onClose} url="/about" title="About" />

                            <HStack
                                justifyContent={"space-evenly"}
                                position={"absolute"}
                                bottom={"2rem"}
                                width={"80%"}
                            >
                                {isAuthenticated ? (
                                    <>
                                        <VStack>
                                            <HStack>
                                                <Link to="/profile" onClick={onClose}>
                                                    <Button colorScheme="yellow" variant={"ghost"}>
                                                        Profile
                                                    </Button>
                                                </Link>
                                                <Button variant={"ghost"} onClick={logoutHandler}>
                                                    <RiLogoutBoxLine />
                                                    Logout
                                                </Button>
                                            </HStack>
                                            {user && user.role === "admin" && (
                                                <Link to={"/admin/dashboard"} onClick={onClose}>
                                                    <Button colorScheme="purple" variant={"ghost"}>
                                                        <RiDashboardFill style={{ margin: "4px" }} />
                                                        Dashboard
                                                    </Button>
                                                </Link>
                                            )}
                                        </VStack>
                                    </>
                                ) : (
                                    <>
                                        <Link to="/login" onClick={onClose}>
                                            <Button colorScheme="yellow">Login</Button>
                                        </Link>
                                        <p>OR</p>
                                        <Link to="/signup" onClick={onClose}>
                                            <Button colorScheme="yellow">Sign up</Button>
                                        </Link>
                                    </>
                                )}
                            </HStack>
                        </VStack>
                    </DrawerBody>
                </DrawerContent>
            </Drawer>
        </>
    );
};

export default Header;
JavaScript
// src/components/Footer/Footer.jsx

import { Box, HStack, Heading, Stack, VStack } from "@chakra-ui/react";
import React from "react";
import {
    TiSocialYoutubeCircular,
    TiSocialInstagramCircular,
} from "react-icons/ti";

const Footer = () => {
    return (
        <Box
            position="flex"
            bottom="0"
            left="0"
            right="0"
            padding="4"
            bg="blackAlpha.900"
            minH="10vh"
        >
            <Stack direction={["column", "row"]}>
                <VStack alignItems={["center", "flex-start"]} width="full">
                    {/* Add any content you want in the VStack */}
                </VStack>
                <Heading children="All Right Reserved" color="white" />
                <Heading children="@Course" fontFamily="body" color="yellow.400" />
                <HStack
                    spacing={["2", "10"]}
                    justifyContent="center"
                    color="white"
                    fontSize="50"
                >
                    <a
                    href="https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2F2.zoppoz.workers.dev%3A443%2Fhttps%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26app%3Ddesktop%26hl%3Den%26next%3Dhttps%253A%252F%252Fstudio.youtube.com%252Fchannel%252FUCWcAS836mcZwSmecex5ZsHQ%252Fvideos%252Fupload%26feature%3Dredirect_login&hl=en&ifkv=AdBytiMMlqFbHmlO6yWJvwcDhnyZgiekbaMvIeLJnQnwl_b5nBUaSNsK-yog-6rD1Jy34ibJkIq2jA&passive=true&service=youtube&uilel=3&flowName=WebLiteSignIn&flowEntry=ServiceLogin&dsh=S-618760585%3A1753274055292796

filter=%5B%5D&sort=%7B%22columnType%22%3A%22date%22%2C%22sortOrder%22%3A%22DESCENDING%22%7D"
                        target="_blank"
                        rel="noopener noreferrer"
                    >
                        <TiSocialYoutubeCircular />
                    </a>
                    <a
                        href="https://accounts.google.com/v3/signin/identifier?continue=https%3A%2F%2F2.zoppoz.workers.dev%3A443%2Fhttps%2Fwww.youtube.com%2Fsignin%3Faction_handle_signin%3Dtrue%26app%3Ddesktop%26hl%3Den%26next%3Dhttps%253A%252F%252Fstudio.youtube.com%252Fchannel%252FUCWcAS836mcZwSmecex5ZsHQ%252Fvideos%26feature%3Dredirect_login&hl=en&ifkv=AdBytiNc2tNu3qEXqf3hpuKYtGmquvtz-W16l79Ql_3YnOejiTCowJzDcXCwLdVcmUU-HOGtUNWSNw&passive=true&service=youtube&uilel=3&flowName=WebLiteSignIn&flowEntry=ServiceLogin&dsh=S117544077%3A1753274055287045"
                        target="_blank"
                        rel="noopener noreferrer"
                    >
                        <TiSocialInstagramCircular />
                    </a>
                </HStack>
            </Stack>
        </Box>
    );
};

export default Footer;
JavaScript
// src/components/Post/PostList.jsx

import React, { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
import axios from 'axios';
import {
    Box,
    Heading,
    Text,
    SimpleGrid,
    Card,
    CardBody,
    CardFooter,
    Button,
    Spinner,
} from '@chakra-ui/react';

function PostList() {
    const [posts, setPosts] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        const fetchPosts = async () => {
            try {
                const response = await axios.get('http://localhost:4000/api/v1/posts');
                setPosts(response.data);
            } catch (error) {
                console.error('Error fetching posts:', error);
            } finally {
                setLoading(false);
            }
        };

        fetchPosts();
    }, []);

    if (loading) {
        return (
            <Box textAlign="center" p={5}>
                <Spinner size="xl" />
                <Text mt={4}>Loading posts...</Text>
            </Box>
        );
    }

    return (
        <Box p={5}>
            <Heading mb={6}>Discussion Board</Heading>
            <SimpleGrid columns={[1, 2, 3]} spacing={5}>
                {posts.map((post) => (
                    <Card key={post._id} borderWidth="1px" borderRadius="lg">
                        <CardBody>
                            <Link to={`/posts/${post._id}`}>
                                <Heading size="md" mb={2}>{post.title}</Heading>
                                <Text noOfLines={3}>{post.content}</Text>
                            </Link>
                        </CardBody>
                        <CardFooter>
                            <Button as={Link} to={`/posts/${post._id}`} 
                            variant="solid" colorScheme="teal">
                                Read More
                            </Button>
                        </CardFooter>
                    </Card>
                ))}
            </SimpleGrid>
        </Box>
    );
}

export default PostList;
JavaScript
// src/components/Post/PostDetail.jsx

import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import axios from 'axios';
import {
    Box,
    Center,
    Heading,
    Text,
    Textarea,
    Button,
    VStack,
    HStack,
    List,
    ListItem,
    Spinner,
} from '@chakra-ui/react';

function PostDetail() {
    const { id } = useParams();
    const [post, setPost] = useState(null);
    const [replyContent, setReplyContent] = useState('');
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        const fetchPost = async () => {
            try {
                const response = await axios.get(`http://localhost:4000/api/v1/posts/${id}`);
                setPost(response.data);
            } catch (error) {
                console.error('Error fetching post:', error);
            } finally {
                setLoading(false);
            }
        };

        fetchPost();
    }, [id]);

    const handleReplySubmit = async () => {
        try {
            const response = await axios.post(`http://localhost:4000/api/v1/posts/${id}/replies`, {
                postId: id,
                content: replyContent,
                author: 'Student', // You can modify this to get the actual author
            });
            setPost((prev) => ({
                ...prev,
                replies: [...prev.replies, response.data],
            }));
            setReplyContent('');
        } catch (error) {
            console.error('Error submitting reply:', error);
        }
    };

    if (loading) return <Center><Spinner size="xl" /></Center>;
    if (!post) return <Center><Text>Post not found.</Text></Center>;

    return (
        <Center>
            <Box p={5} maxWidth="600px" width="100%">
                <Heading mb={4}>{post.title}</Heading>
                <Text mb={4}>{post.content}</Text>

                <Heading size="md" mb={2}>Replies:</Heading>
                <List spacing={3}>
                    {post.replies.map((reply) => (
                        <ListItem key={reply._id} borderWidth="1px" borderRadius="md" p={3}>
                            <Text fontWeight="bold">{reply.author}</Text>
                            <Text>{reply.content}</Text>
                        </ListItem>
                    ))}
                </List>

                <VStack spacing={4} mt={6} align="stretch">
                    <Heading size="md">Leave a reply:</Heading>
                    <Textarea
                        value={replyContent}
                        onChange={(e) => setReplyContent(e.target.value)}
                        placeholder="Write your reply here..."
                        size="sm"
                        resize="vertical"
                    />
                    <HStack justify="flex-end">
                        <Button colorScheme="teal" onClick={handleReplySubmit}>
                            Submit
                        </Button>
                    </HStack>
                </VStack>
            </Box>
        </Center>
    );
}

export default PostDetail;


Start using the below command:

npm start

Output:


Article Tags :

Similar Reads