While programming, programmers encounter various challenges, which make them solicit help to solve these problems. Forums provide a tech community that can assist with these problems. We will be building a forum site with NextJs on the Front-end and Strapi for content management.
This tutorial will cover building a forum website and providing user authentication and authorization on the site.
To fully grasp this tutorial, you need:
Strapi is an open-source headless CMS built on Node.js. Strapi enables developers to create and manage the content of their applications. Strapi provides users with an admin panel that is used to manage the user's content. Content can be created with tables as in a traditional database. What's more? Strapi provides functionality to integrate APIs and allows collaborative access to content to enable multiple users to access and manage stored data.
To set up our Strapi backend, we will first create a directory for our project:
1 mkdir forumapp
This creates a folder for our project strapi-forum
. Next, we move into this folder:
cd forumapp
Then, we install Strapi:
npx create-strapi-app@latest forum-backend --quickstart
The command above sets up Strapi for our app with all dependencies required. Here, forum-backend
is the name of our project folder. The --quickstart
option sets up Strapi with a sqlite
database.
Once the process is complete, it runs strapi develop
and opens up an administrator panel on localhost. If you open up your browser to the URL, you will get a form.
Fill out your details in the form and click on "Let's start." It will sign you in and navigate you to the dashboard.
In this section, we will create our collection to contain the post
and comments
for our forum application.
strapi-forum
.We now have five fields: the title
, answers
, questions
, username
and answername
for our forum posts.
save
and publish
buttons.The new entry will be added to the "Strapi Forums" collection.
You can fetch the data from the CollectionCollection with an API tester. Enter the URL: http://localhost:1337/api/strapi-forums. Send a request, and you'll get the response from Strapi:
We will build the front-end of our forum application with Next.js. Next.js is an open-source framework built on Node.js to allow React applications to be rendered on the server-side.
To install Next.js:
npx create-next-app forum
The above command installs the Next.js framework in a project folder forum. Our finished app will have two pages: one to display the forum and the other to post new questions. The images below show what our app will look like:
The display forum page:
Post a new question page:
In the forum
project folder, our app structure will look like this:
1┣ 📂pages
2 ┃ ┣ 📂api
3 ┃ ┃ ┗ 📜hello.js
4 ┃ ┣ 📂Components
5 ┃ ┃ ┣ 📜Displayforum.js
6 ┃ ┃ ┗ 📜Uploadforum.js
7 ┃ ┣ 📜index.js
8 ┃ ┣ 📜upload.js
9 ┃ ┗ 📜_app.js
10 ┣ 📂public
11 ┃ ┣ 📜favicon.ico
12 ┃ ┗ 📜vercel.svg
13 ┣ 📂styles
14 ┃ ┣ 📜globals.css
15 ┃ ┗ 📜Home.module.css
Here, our index.js
file is our display form page, and it makes use of the component Displayforum.js
while the upload.js
file serves as our page for posting new questions. It contains the component Uploadforum.js
. All our styles are in Home.module.css
.
In Index.js
we have the following codes:
1 import styles from "../styles/Home.module.css";
2 import Displayforum from "./Components/Displayforum";
3 export default function Home() {
4 return (
5 <div className={styles.container}>
6 <Displayforum />
7 </div>
8 );
9 }
Here, we have added the Displayforum
component to our page. In Displayforum.js
, we have:
1 import React, { useState } from "react";
2 import style from "../../styles/Home.module.css";
3 import Link from "next/link";
4 function Displayforum() {
5 const [show, setShow] = useState(false);
6 return (
7 <div>
8 <div className={style.topcont}>
9 <h1 className={style.heading}>Display forum</h1>
10 <div>
11 <Link href="/upload">
12 <button>Ask a question</button>
13 </Link>
14 <button>Login</button>
15 </div>
16 </div>
17 <h2 className={style.subheading}>Questions</h2>
18 <div className={style.userinfo}>
19 <p>Posted By: Victory Tuduo</p>
20 </div>
21 <div className={style.questioncont}>
22 <p className={style.question}>Description of the Question</p>
23 </div>
24 <div className={style.answercont}>
25 <h2 className={style.subheading}>Answers</h2>
26 <div className={style.inputanswer}>
27 <form>
28 <textarea type="text" placeholder="Enter your answer" rows="5" />
29 <button>Post</button>
30 </form>
31 </div>
32 <button className={style.showanswer} onClick={() => setShow(!show)}>
33 {show ? "Hide Answers" : "Show Answers"}
34 </button>
35 {show ? (
36 <div className={style.answers}>
37 <div className={style.eachanswer}>
38 <p className={style.username}>Miracle</p>
39 <p className={style.answertext}>Try doing it Like this</p>
40 </div>
41 </div>
42 ) : null}
43 </div>
44 </div>
45 );
46 }
47 export default Displayforum;
This component handles the layout of our display forum page. We also have a button here that directs the user to the page to upload new questions. Meanwhile, in upload.js
we have the following:
1 import React from "react";
2 import Uploadforum from "./Components/Uploadforum";
3 function upload() {
4 return (
5 <div>
6 <Uploadforum />
7 </div>
8 );
9 }
10 export default upload;
Here, we simply added an import for the Uploadforum
component into our page. In Uploadforum.js
file we have a simple form to create new questions:
1 import React from "react";
2 import style from "../../styles/Home.module.css";
3 import Link from "next/Link";
4 function Uploadforum() {
5 return (
6 <div className={style.uploadpage}>
7 <div className={style.topcont}>
8 <h1>Ask a question</h1>
9 <Link href="/">
10 <button>Forum</button>
11 </Link>
12 </div>
13 <div className={style.formcont}>
14 <form className={style.uploadform}>
15 <input type="text" placeholder="Enter your title" maxLength="74" />
16 <textarea type="text" placeholder="Enter your description" rows="8" />
17 <button>Submit Question</button>
18 </form>
19 </div>
20 </div>
21 );
22 }
23 export default Uploadforum;
Finally, we have the following styles in Home.module.css
:
1 .container {
2 min-height: 100vh;
3 padding: 0 0.5rem;
4 height: 100vh;
5 font-family: monospace;
6 }
7 /* display forum page */
8 .topcont {
9 display: flex;
10 justify-content: space-between;
11 align-items: center;
12 padding: 5px 8px;
13 }
14 .topcont button,
15 .inputanswer button,
16 .formcont button,
17 .showanswer {
18 border: none;
19 color: #fff;
20 background: dodgerblue;
21 border-radius: 8px;
22 padding: 10px 15px;
23 outline: none;
24 margin: 8px;
25 }
26 .topcont button:hover {
27 cursor: pointer;
28 transform: scale(1.2);
29 }
30 .heading {
31 font-weight: bold;
32 }
33 .subheading {
34 font-weight: 500;
35 text-transform: uppercase;
36 }
37 .userinfo {
38 font-size: 18px;
39 font-weight: 600;
40 }
41 .questioncont {
42 min-height: 300px;
43 padding: 15px 14px;
44 box-shadow: 12px 12px 36px rgba(0, 0, 0, 0.12);
45 }
46 .answercont {
47 min-height: 300px;
48 padding: 5px 3px 5px 15px;
49 }
50 .answers {
51 height: 300px;
52 overflow-x: scroll;
53 }
54 .inputanswer {
55 margin-bottom: 8px;
56 }
57 .inputanswer textarea {
58 width: 100%;
59 resize: none;
60 padding: 5px 8px;
61 }
62 .showanswer {
63 border: 1px solid dodgerblue;
64 background: #fff;
65 color: dodgerblue;
66 transition: 0.4s ease-in-out;
67 }
68 .showanswer:hover {
69 background: dodgerblue;
70 color: #fff;
71 }
72 .eachanswer {
73 border-radius: 15px;
74 background: #e7e7e7;
75 padding: 8px 15px;
76 margin-bottom: 10px;
77 }
78 .username {
79 font-weight: bold;
80 text-transform: uppercase;
81 }
82 .answertext {
83 font-family: Montserrat;
84 font-size: 14px;
85 font-weight: 500;
86 }
87 /* upload a question page */
88 .uploadpage {
89 min-height: 100vh;
90 }
91 .formcont {
92 min-width: 100vw;
93 display: flex;
94 justify-content: center;
95 align-items: center;
96 }
97 .uploadform {
98 display: flex;
99 flex-direction: column;
100 min-width: 500px;
101 padding-top: 10px;
102 }
103 .uploadform input,
104 .uploadform textarea {
105 resize: none;
106 width: 100%;
107 margin: 8px;
108 padding: 5px;
109 }
All of this makes up the layout of our pages.
In this phase, we will fetch the data from Strapi as the next step.
In this section, we will fetch our data from Strapi and display it in our app. We will be using Axios to perform our fetch operations.
We will install this via CLI:
npm install axios
Create a file index.js
in the API folder. Here, we will set up our fetch request:
1 import axios from "axios";
2 const url = "http://localhost:1337/api/strapi-forums";
3 export const readForum = () => axios.get(url);
Above, we added import for axios
, the URL to fetch our data, and exported functions to read and create data from our forum. We’ll import these functions into our app in our index.js
file:
1 import { readForum, createQuestion } from "./api";
We will fetch the data from Strapi in our index.js
file and pass it to Displayforum.js
component to display it:
1 import { react, useState, useEffect } from "react";
2 ...
3 const [question, setQuestions] = useState({});
4 const [response, setResponse] = useState([]);
5 useEffect(() => {
6 const fetchData = async () => {
7 const result = await readForum();
8 setResponse(result.data.data);
9 };
10 fetchData();
11 }, []);
Here, we fetched our data from Strapi and assigned it to response
with the React useState
hook. We have a useEffect
function that makes the request when our component mounts. Now, we pass this response
down to our Displayforum
component.
1 <Displayforum response={response} />
To display our data in our Displayforum.js
file, we will map our responses and render our components. We will handle this in the Displayforum
component:
1 //...
2 function Displayforum({ response }) {
3 //...
4 {response.map((response, index) => (
5 <div key={index}>
6 <h2 className={style.subheading}>{response.attributes.Title}</h2>
7 <div className={style.userinfo}>
8 //...
9 <p className={style.answertext}>Try doing it Like this</p>
10 </div>
11 </div>
12 ) : null}
13 </div>
14 </div>
15 ))}
Here, we wrapped up our components to map through response
and display this component as many times as the number of responses. To display our Strapi data, we simply reference it. We can get our Username
with this code:
1 response.attributes.Username
We can now add this to our component and display it:
1 <p>Posted By: {response.attributes.Username}</p>
2 ...
3 <p className={style.question}>{response.attributes.Questions}</p>
4 ...
To display our answers, we will map through the Answers
content returned by Strapi:
1 {response.attributes.Answers.map((answers, i) => (
2 <div className={style.eachanswer} key={i}>
3 <p className={style.username}>{response.attributes.Answername}</p>
4 <p className={style.answertext}>{answers}</p>
5 </div>
6 ))}
We have successfully added the data from our CollectionCollection to our front-end
to view this in the browser. Run the following command in the CLI:
npm run dev
In your browser, you will have an output similar to the image below:
After this, we will add functionality to add new questions to Strapi.
In our Uploadforum.js
file, we will add functionality to upload the contents of the form to Strapi. First, we will create two state variables to store the text from our inputs
.
1 import { React, useState } from "react";
2 ...
3 const [name, setName] = useState("");
4 const [description, setDescription] = useState("");
Then we set these variables to the value of our form input
.
1 <input
2 type="text"
3 placeholder="Enter your title"
4 maxLength="74"
5 value={name}
6 onChange={(e) => setName(e.target.value)}
7 />
8 <textarea
9 type="text"
10 placeholder="Enter your description"
11 rows="8"
12 value={description}
13 onChange={(e) => setDescription(e.target.value)}
14 />
Also, we will add a function to send these variables when we click our button
.
1 <button onClick={(e) => {
2 e.preventDefault();
3 sendData();
4 }}
5 }>Submit Question</button>
We will create a sendData
function above the return
statement to handle sending the newly-created questions to Strapi.
1 const sendData = () => {
2 };
For our create functionality we will import the createQuestion
function we defined in our api
folder.
1import axios from "axios";
Then we pass in our data to this function.
1 const url = "http://localhost:1337/api/strapi-forums";
2 const sendData = () => {
3 axios.post(url, {
4 data: {
5 Title: name,
6 Questions: description,
7 },
8 });
We can now upload new questions to our Strapi collection
. We will add the Username
when we cover user authentication.
Next up, we will add functionality to answer questions in our Displayforum
component.
We will repeat the same method as we did with our Upload Question
functionality for the add answer functionality, but with a minor difference. In your Displayforum
component, add the following code:
1 import axios from "axios";
2 //...
3 const [answer, setAnswer] = useState("")
4 const [id, setId] = useState("");
5 const [a, formerArray] = useState([]);
We will store the input from the textarea
in answer
. We will use the id
variable to reference the collection we want to add the answer to.
Then in our form textarea
:
1 <textarea
2 type="text"
3 placeholder="Enter your answer"
4 rows="5"
5 value={answer}
6 onChange={(e) => {
7 formerArray(response.attributes.Answers);
8 setAnswer(e.target.value);
9 setId(response.id);
10 }}
11 />
12 <button
13 onClick={(e) => {
14 submitAnswer();
15 }}
16 >
17 }}>Post</button>
Then in the submitAnswer
function:
1 const submitAnswer = () => {
2 try {
3 axios.put(`http://localhost:1337/api/strapi-forums/${id}`, {
4 Answers: [...a, answer],
5 });
6 } catch (error) {
7 console.log(error);
8 }
9 };
With this, we can now add answers through our form to our collection.
This section will use Nextauth, a NextJs package for authentication, to implement Google login for our application. We will also set up protected routes so that only authenticated users can create questions and view them.
To install next-auth
, run:
npm i next-auth
For our authentication, we will make use of JWT token . JWT is a standard used to create access tokens for an application. We will create a file to handle user authentication. To do this, create a folder named auth
in your api
folder and within it, create a file [...nextauth].js
with the following code in it:
1 import NextAuth from "next-auth"
2 import GoogleProvider from "next-auth/providers/google"
3
4
5 export default NextAuth({
6 secret: process.env.SECRET,
7 providers: [
8 GoogleProvider({
9 clientId: process.env.GOOGLE_ID,
10 clientSecret: process.env.GOOGLE_SECRET,
11 authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code',
12 }),
13 ],
14 })
The code above sets up our Google Authentication
for our app. To use it, we need to wrap up our application in _app.js
with the Google Provider component:
1 ...
2 import { SessionProvider } from "next-auth/react"
3 function MyApp({ Component, pageProps }) {
4 return (
5 <SessionProvider session={pageProps.session}>
6 <Component {...pageProps} />
7 </SessionProvider>
8 );
9 }
10 export default MyApp;
Next, we will modify our Displayforum
component in to return to our component if the user is authenticated, else it returns a button that leads to an authentication page:
1 import {signIn, signOut, useSession} from 'next-auth/react'
2 //...
3 const { data: session } = useSession()
We will use useSession
to know if our user has been authorized. If there is session
, we will return the rest of the component and if there is no session
we will render a button to sign in
to access the app.
1 return(
2 <div>
3 {!session && (
4 <>
5 <h1>Sign in to access forum</h1>
6 <button onClick={() => signIn()}>Sign In</button>
7 </>
8 )}
9 {session && (
10 <>
11 {/*rest of our app*/}
12 </>
13 )}
14 </div>
We will also set our button to Sign out
:
1 //...
2 <Link href="/upload">
3 <button>Ask a question</button>
4 </Link>
5 <button onClick={() => signOut()}>Signout</button>
6 //...
To make use of Google authentication
in our app, we will require access credentials from Google Cloud console. To get this, navigate in your browser to Google Cloud.
Click on OAuth Client ID
and Fill out the fields on the new page that opens.
Finally, set the redirect URL to: http://localhost/api/auth/callback/google
To use the credentials in the […nextauth].js
file, you can create a .env
file and set up your environmental variables:
1 GOOGLE_CLIENT_ID: id
2 GOOGLE_CLIENT_SECRET: secret
3 secret: any string
Next, we will set up our Uploadforum.js
component on our upload
page as a protected route so that unauthorized users can’t access the route. To do this, in upload.js
add the following code:
1 import { getSession, useSession } from 'next-auth/react'
Then at the bottom:
1 export async function getServerSideProps(context) {
2 const session = await getSession(context);
3 if (!session) {
4 context.res.writeHead(302, { Location: '/' });
5 context.res.end();
6 return {};
7 }
8 return {
9 props: {
10 user: session.user,
11 }
12 }
13 }
14 export default Upload;
Now, if you run the app with npm run dev
in CLI, we have Google authentication
implemented. Also we can’t access the /upload
path without logging in.
Now that we have added authentication to our app, we can add the username
received from the Google Login
as the Answername
field when we answer a question:
1 //...
2 axios.put(`http://localhost:1337/api/strapi-forums/${id}`, {
3 Answers: [...a, answer],
4 Answername: session.user.name,
5 });
Now, if I add a new answer to the form:
When I click on the Post
button, I get:
The answer has been added and the Answername
field has been set to my user.name
form our session
.
Finally, we will also add the username
when posting a question to our collection
. We will do this in our upload.js
file:
1 const { data: session } = useSession();
Then we pass the value of session
to our Uploadforum
Component:
1 <Uploadforum session={session} />
We can now use session
data in our Uploadforum
component:
1 function Uploadforum({session}) {
2 //...
3 axios.post(url, {
4 data: {
5 Title: name,
6 Questions: description,
7 Answers: [],
8 Username: session.user.name,
9 },
10 });
Any new questions added now take the Username
field to be the username
received from session
. If we add new answers, since the Answername
is a field, it overwrites the previous data and all the answers
use the same name. To fix this, we will simply modify our Answers
field of type JSON to contain both the answers and the username of the person providing the answers.
Then, we can get this data and display it in our Displayforum
component:
1 <div className={style.answers}>
2 {response.attributes.Answers.map((answers, i) => (
3 <div className={style.eachanswer} key={i}>
4 <p className={style.username}>{answers[0]}</p>
5 <p className={style.answertext}>{answers[1]}</p>
6 </div>
7 ))}
answer[0]
is the name of the user, while answers[1]
is the answer.
Finally, we will modify the code to add new answers:
1 ...
2 axios.put(`http://localhost:1337/api/strapi-forums/${id}`, {
3 Answers: [...a, [session.user.name, answer]],
4 });
5 } catch (error) {
6 console.log(error);
We can now add new answers to our questions without overwriting previous data.
When I click on post I get a new answer:
We have come to the end of this tutorial. In this tutorial, we learned how to use Strapi CMS and connect it to NextJ's front-end. In this process, we built a forum site and implemented user authentication and authorization on it.
The source code used in this tutorial can be found in the GitHub repo: Forum Application.
I am a front-end react developer and technical writer. I build scalable web applications with different implementations.