In this article, you'll learn how to build a forum system that allows users to create, react, and reply to post threads. In the end, we will also send a notification on each reply on a thread with Novu, you can skip the last step if you want only the…
Author:
Nevo David
I know, there are no forums today like before, it’s all about the Reddit, Facebook communities (and smaller ones like Devto and Mastodon), but! Once upon a time, when I was a boy, I was addicted to forums, I have actually created a few with PHPBB and vBulletin, back when PHP was a thing. being so nostalgic made me write this blog post 😎
Here, I’ll guide you through creating the project environment for the web application. We’ll use React.js for the front end and Node.js for the backend server.
Create the project folder for the web application by running the code below:
mkdir forum-systemcd forum-systemmkdir client server
Navigate into the server folder and create a package.json file.
cd server & npm init -y
Install Express, Nodemon, and the CORS library.
npm install express cors nodemon
ExpressJS is a fast, minimalist framework that provides several features for building web applications in Node.js, CORS is a Node.js package that allows communication between different domains, and Nodemon is a Node.js tool that automatically restarts the server after detecting file changes.
Create an index.js file – the entry point to the web server.
touch index.js
Set up a Node.js server using Express.js. The code snippet below returns a JSON object when you visit the http://localhost:4000/api in your browser.
Update the Nav.js file to render a navigation bar that shows the application’s title and a sign-out button. Later in this article, I’ll guide you through adding Novu’s notification bell within this component.
import React from "react";const Nav = () => { const signOut = () => { alert("User signed out!"); }; return ( Threadify</h2> Sign out</button> </div> </nav> );};export default Nav;
It is the home page after the user’s authentication. They can create and react to posts (threads) within the Home component. Copy the code below into the Home.js file.
import React, { useState } from "react";import Nav from "./Nav";const Home = () => { const [thread, setThread] = useState(""); const handleSubmit = (e) => { e.preventDefault(); console.log({ thread }); setThread(""); }; return ( <> Create a Thread</h2> Title / Description</label> setThread(e.target.value)} /> </div> CREATE THREAD</button> </form> </main> </> );};export default Home;
The code snippet above displays the navigation bar and an input field for the posts. We’ll update the component in the upcoming sections.
Here, I’ll guide you through authenticating users and how to allow only authorised users to access protected pages within the web application.
PS: In a real-world application, passwords are hashed and saved in a secure database. For simplicity purposes, I’ll store all the credentials in an array in this tutorial.
Add a POST route on the server that accepts the user’s credentials – email, username, and password.
//\ud83d\udc47\ud83c\udffb holds all the existing usersconst users = [];//\ud83d\udc47\ud83c\udffb generates a random string as IDconst generateID = () => Math.random().toString(36).substring(2, 10);app.post("/api/register", async (req, res) => { const { email, password, username } = req.body; //\ud83d\udc47\ud83c\udffb holds the ID const id = generateID(); //\ud83d\udc47\ud83c\udffb logs all the user's credentials to the console. console.log({ email, password, username, id });});
Create a signUp function within the Register.js file that sends the user’s credentials to the endpoint on the server.
Call the function when a user submits the form as done below:
const handleSubmit = (e) => { e.preventDefault(); //\ud83d\udc47\ud83c\udffb Triggers the function signUp(); setEmail(""); setUsername(""); setPassword("");};
Update the /api/register route to save the user’s credentials and return a response to the front-end.
app.post("/api/register", async (req, res) => { const { email, password, username } = req.body; const id = generateID(); //\ud83d\udc47\ud83c\udffb ensures there is no existing user with the same credentials const result = users.filter( (user) => user.email === email && user.password === password ); //\ud83d\udc47\ud83c\udffb if true if (result.length === 0) { const newUser = { id, email, password, username }; //\ud83d\udc47\ud83c\udffb adds the user to the database (array) users.push(newUser); //\ud83d\udc47\ud83c\udffb returns a success message return res.json({ message: "Account created successfully!", }); } //\ud83d\udc47\ud83c\udffb if there is an existing user res.json({ error_message: "User already exists", });});
The code snippet above accepts the user’s credentials from the React.js application and checks if there is no existing user with the same credentials before saving the user to the database (array).
Finally, display the server’s response by updating the signUp function as done below.
The code snippet above displays a success or error message from the Node.js server and redirects the user to the login page after successfully creating the account.
Add a POST route on the server that accepts the user’s email and password and authenticates the users before granting them access to the web application.
app.post("/api/login", (req, res) => { const { email, password } = req.body; //\ud83d\udc47\ud83c\udffb checks if the user exists let result = users.filter( (user) => user.email === email && user.password === password ); //\ud83d\udc47\ud83c\udffb if the user doesn't exist if (result.length !== 1) { return res.json({ error_message: "Incorrect credentials", }); } //\ud83d\udc47\ud83c\udffb Returns the id if successfuly logged in res.json({ message: "Login successfully", id: result[0].id, });});
Create a loginUser function within the Login.js file that sends the user’s email and password to the Node.js server.
The code snippet above sends the user’s credentials to the Node.js server and displays the response on the front end. The application redirects authenticated users to the Home component and saves their id to the local storage for easy identification.
Update the signOut function within the Nav.js file to remove the id from the local storage when a user logs out.
const signOut = () => { localStorage.removeItem("_id"); //\ud83d\udc47\ud83c\udffb redirects to the login page navigate("/");};
Next, send the user’s id and the post title to the server. Before then, let’s ensure the Home.js route is protected. Add a useEffect hook within the Home component to determine whether the user is authenticated.
When users sign in to the application, we save their id to the local storage for easy identification. The code snippet above checks if the id exists; otherwise, the user is redirected to the login page.
Add a function within the Home component that sends the user’s id and the post title to the Node.js server when the form is submitted.
const createThread = () => { fetch("http://localhost:4000/api/create/thread", { method: "POST", body: JSON.stringify({ thread, userId: localStorage.getItem("_id"), }), headers: { "Content-Type": "application/json", }, }) .then((res) => res.json()) .then((data) => { console.log(data); }) .catch((err) => console.error(err));};//\ud83d\udc47\ud83c\udffb Triggered when the form is submittedconst handleSubmit = (e) => { e.preventDefault(); //\ud83d\udc47\ud83c\udffb Calls the function createThread(); setThread("");};
Save the post and send all the available posts to the client for display.
//\ud83d\udc47\ud83c\udffb holds all the posts createdconst threadList = [];app.post("/api/create/thread", async (req, res) => {const { thread, userId } = req.body;const threadId = generateID(); //\ud83d\udc47\ud83c\udffb add post details to the array threadList.unshift({ id: threadId, title: thread, userId, replies: [], likes: [], }); //\ud83d\udc47\ud83c\udffb Returns a response containing the posts res.json({ message: "Thread created successfully!", threads: threadList, });});
The code snippet above accepts the user’s id and post title from the front end. Then, save an object that holds the post details and returns a response containing all the saved posts.
The code snippet contains the SVG element for the Comments button and the number of comments on the post.The handleAddComment function is triggered when a user clicks the comment icon. It redirects the user to the Replies component where they can view and add to the replies on each post.
So far, we’ve been able to display the available posts only when we create a new post. Next, let’s retrieve them when the component is mounted.
Add a GET route on the server that returns all the posts.
Create a POST route on the server that validates the reaction.
app.post("/api/thread/like", (req, res) => { //\ud83d\udc47\ud83c\udffb accepts the post id and the user id const { threadId, userId } = req.body; //\ud83d\udc47\ud83c\udffb gets the reacted post const result = threadList.filter((thread) => thread.id === threadId); //\ud83d\udc47\ud83c\udffb gets the likes property const threadLikes = result[0].likes; //\ud83d\udc47\ud83c\udffb authenticates the reaction const authenticateReaction = threadLikes.filter((user) => user === userId); //\ud83d\udc47\ud83c\udffb adds the users to the likes array if (authenticateReaction.length === 0) { threadLikes.push(userId); return res.json({ message: "You've reacted to the post!", }); } //\ud83d\udc47\ud83c\udffb Returns an error user has reacted to the post earlier res.json({ error_message: "You can only react once!", });});
From the code snippet above:
The route accepts the post id and the user’s id from the React.js application and searches for the post that received a reaction.
Then, validates the reaction before adding the user to the likes array.
Update the handleLikeFunction to send a POST request to the api/thread/like endpoint whenever a user reacts to the post.
Here, I’ll guide you through displaying the replies on each post within the React app.
Add a POST route on the server that accepts a post ID from the front end and returns the title and all the responses under such post.
app.post("/api/thread/replies", (req, res) => { //\ud83d\udc47\ud83c\udffb The post ID const { id } = req.body; //\ud83d\udc47\ud83c\udffb searches for the post const result = threadList.filter((thread) => thread.id === id); //\ud83d\udc47\ud83c\udffb return the title and replies res.json({ replies: result[0].replies, title: result[0].title, });});
Next, update the Replies component to send a request to the api/thread/replies endpoint on the server and display the title and replies when the page loads.
Create an endpoint on the server that allows users to add new replies as done below:
app.post("/api/create/reply", async (req, res) => { //\ud83d\udc47\ud83c\udffb accepts the post id, user id, and reply const { id, userId, reply } = req.body; //\ud83d\udc47\ud83c\udffb search for the exact post that was replied to const result = threadList.filter((thread) => thread.id === id); //\ud83d\udc47\ud83c\udffb search for the user via its id const user = users.filter((user) => user.id === userId); //\ud83d\udc47\ud83c\udffb saves the user name and reply result[0].replies.unshift({ userId: user[0].id, name: user[0].username, text: reply, }); res.json({ message: "Response added successfully!", });});
The code snippet above accepts the post id, user id, and reply from the React app, searches for the post via its ID, and adds the user’s id, username, and reply to the post replies.
Create a function that sends a request to the /api/create/reply endpoint.
Congratulations on making it thus far! In the upcoming sections, you’ll learn how to send notifications to multiple users using the Topics API provided by Novu.
In this section, we’ll use the Novu Topic API to send notifications to multiple users simultaneously. To do this, the API allows us to create a unique topic, assign subscribers to the topic, and send a bulk notification to the subscribers at once.
Navigate into the client folder and create a Novu project by running the code below.
cd clientnpx novu init
Select your application name and sign in to Novu. The code snippet below contains the steps you should follow after running npx novu init.
Now let's setup your account and send your first notification? What is your application name? Forum App? Now lets setup your environment. How would you like to proceed? Create a free cloud account (Recommended)? Create your account with: Sign-in with GitHub? I accept the Terms and Conditions (https://novu.co/terms) and have read the Privacy Policy (https://novu.co/privacy) Yes\u2714 Created your account successfully. We've created a demo web page for you to see novu notifications in action. Visit: http://localhost:51422/demo to continue
Visit the demo page, copy your subscriber ID from the page, and click the Skip Tutorial button.
Create a notification template with a workflow as shown below:
Update the In-App notification template to send this message to the post creator when there is a new reply.
Someone just dropped a reply to the thread!
Install the Novu Notification package within your React project.
npm install @novu/notification-center
Update the components/Nav.js file to contain the Novu notification bell according to its documentation.
The code snippet above adds Novu’s notification bell icon to the Nav component, enabling us to view all the notifications in our app. Replace the Subscriber ID variable with yours.
Select Settings on your Novu Admin Panel and copy your App ID and API key. Ensure you add the App ID into its variable within the Nav.js file.
To begin with, you’ll need to add the users as subscribers when they login to the application. Therefore, update the /api/register route to add the user as a Novu subscriber.
app.post("/api/register", async (req, res) => { const { email, password, username } = req.body; const id = generateID(); const result = users.filter( (user) => user.email === email && user.password === password ); if (result.length === 0) { const newUser = { id, email, password, username }; //\ud83d\udc47\ud83c\udffb add the user as a subscriber await novu.subscribers.identify(id, { email: email }); users.push(newUser); return res.json({ message: "Account created successfully!", }); } res.json({ error_message: "User already exists", });});
The code snippet above creates a Novu subscriber via the user’s email and id.
Next, add each new post as a Novu topic and add the user as a subscriber to the topic.
app.post("/api/create/thread", async (req, res) => { const { thread, userId } = req.body; let threadId = generateID(); threadList.unshift({ id: threadId, title: thread, userId, replies: [], likes: [], });//\ud83d\udc47\ud83c\udffb creates a new topic from the post await novu.topics.create({ key: threadId, name: thread, });//\ud83d\udc47\ud83c\udffb add the user as a subscriber await novu.topics.addSubscribers(threadId, { subscribers: [userId], //replace with your subscriber ID to test run // subscribers: [""], }); res.json({ message: "Thread created successfully!", threads: threadList, });});
Finally, send notifications to the subscriber when there is a new response on the thread
app.post("/api/create/reply", async (req, res) => { const { id, userId, reply } = req.body; const result = threadList.filter((thread) => thread.id === id); const user = users.filter((user) => user.id === userId); result[0].replies.unshift({ name: user[0].username, text: reply });//\ud83d\udc47\ud83c\udffb Triggers the function when there is a new reply await novu.trigger("topicnotification", { to: [{ type: "Topic", topicKey: id }], }); res.json({ message: "Response added successfully!", });});
Congratulations!🎉 You’ve completed the project for this tutorial.
So far, you’ve learnt how to authenticate users, communicate between a React and Node.js app, and send bulk notifications using the Novu Topic API.
Novu is an open-source notification infrastructure that enables you to send SMS, chat, push, and e-mail notifications from a single dashboard. The Topic API is just one of the exciting features Novu provides; feel free to learn more about us here.
The source code for this tutorial is available here: