Simple Journal App with MERN Stack
Simple Journal App is a great way to showcase your understanding of full-stack development. In this project, we will be making a Blogging website using MERN stack (MongoDB, ExpressJS, React, NodeJS). We will be using React.js framework and tailwind CSS for our frontend and express.js and MongoDB as our backend. This project will showcase how to upload, View, save, and make blogs as well as keep a record of users on your site.
Project Preview:
Prerequisites:
Approach:
- Decide on all the functionalities that need to be added in the project
- Chose the required dependencies and modules that need to be added to the project and install them
- Start working on backend
- Make routes needed divide them into user routes and post routes
- start working on front end
- make the required components
- make pages with the said components
- in app.jsx routes which page should lead to where
- link backend and frontend
Steps to Create the Application:
Step 1. Create a folder for the project using the following command.
mkdir blogit
cd blogit
Step 2. Create the backend folder.
mkdir backend
cd backend
Step 3: Initialize the Node application using the following command.
npm init -y
Step 4: Install the required dependencies.
npm install mongoose express jsonwebtoken cors
npm install --save-dev nodemon
Folder Structure(backend):
Dependencies:
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.2.4"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
Example: Create the required files and add the following codes.
//index.js
const UserRouter = require("./routes/user.js")
const express = require("express")
const mongo = require("mongoose");
const PostRouter = require("./routes/post.js");
const cors = require("cors")
const app = express();
app.use(cors())
const port = 8000;
app.use(express.json())
app.listen(port, () => {
console.log(`Server live at http://localhost:${port}`)
})
app.use('/api/v1/user', UserRouter)
app.use('/api/v1/blog', PostRouter)
//db.js
const mongoose = require('mongoose')
mongoose.connect("Your MongoDB URI")
const userSchema = new mongoose.Schema({
Username: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
minLength: 3,
maxLength: 30
},
password: {
type: String,
required: true,
minLength: 8,
},
})
const PostSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
content: {
type: String,
required: true,
},
createdAt: {
type: Date,
default: Date.now
},
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true }
})
const User = mongoose.model("User", userSchema)
const Post = mongoose.model("Post", PostSchema)
module.exports = { User, Post }
//config.js
const JWT_SECRET = "Its Your Secret"
module.exports = { JWT_SECRET }
//routes/user.js
const { JWT_SECRET } = require("../config.js")
const jwt = require("jsonwebtoken")
const express = require("express")
const { User } = require("../db")
const mongo = require("mongoose")
const UserRouter = express.Router();
UserRouter.post('/signup', async (req, res) => {
const existingUser = await User.findOne({
Username: req.body.username
})
if (existingUser) {
return res.status(411).json({
message: "Username already taken/Incorrect inputs"
})
}
try {
const user = await User.create({
Username: req.body.username,
password: req.body.password,
});
const token = jwt.sign({ id: user._id }, JWT_SECRET);
return res.json({ token });
} catch (e) {
res.status(403).json({ e });
}
});
UserRouter.post('/signin', async (req, res) => {
const user = await User.findOne({
Username: req.body.username,
password: req.body.password
})
console.log("d" + user)
if (user) {
const token = jwt.sign({
userId: user._id
}, JWT_SECRET);
res.json({
token: token
})
return;
}
res.status(411).json({
message: "Error while logging in"
})
});
module.exports = UserRouter
//routes/post.js
const { JWT_SECRET } = require("../config.js")
const jwt = require("jsonwebtoken")
const express = require("express")
const { Post, User } = require("../db")
const PostRouter = express.Router();
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(403).json({});
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
req.userId = decoded.userId;
next();
} catch (err) {
return res.status(403).json({});
}
};
PostRouter.use('/*', authMiddleware)
PostRouter.get('/bulk', async (req, res) => {
const blogs = await Post.find({}).populate('author', 'Username');
return res.json({ blogs });
});
PostRouter.post('/post', async (req, res) => {
const userId = req.userId;
const body = req.body;
const post = await Post.create({
title: body.title,
content: body.content,
author: userId,
});
return res.json({
id: post._id,
});
});
PostRouter.get('/uname', async (req, res) => {
const id = req.userId
const user = await User.findById(id)
return (res.json({ uname: user.Username }))
})
PostRouter.get('/:id', async (req, res) => {
const id = req.params.id;
const blog = await Post.findById(id).populate('author', 'Username')
return res.json(blog)
});
module.exports = PostRouter;
To start the backend run the following command.
nodemon index.js
Step 5: Create the frontend application using the following command.
npm create vite@latest
name the project accordingly and chose react for framework and then variant javascript
Step 6: Install all the dependencies we need run the commands in the terminal
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm i axios react-router-dom html-react-parser
npm install --save @tinymce/tinymce-react
Step 7: Replace content of tailwind.config.js file with the below code
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
Step 8. Remove all the content in app.css and index.css
Step 9. Create a file config.js in frontend/src and add the below code in your index.css and config.js. Change the URL in config.js to be that of your Backend server
/*src/index.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
//src/config.js
export const BACKEND_URL = "http://localhost:8000"
Folder Structure:
Dependencies:
"dependencies": {
"@tinymce/tinymce-react": "^5.0.0",
"axios": "^1.6.8",
"html-react-parser": "^5.1.10",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.3"
},
"devDependencies": {
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.6",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"vite": "^5.2.0"
}
Step 10: Create the required files as shown in folder structure and add the following codes.
//components/header.jsx
import { Link } from "react-router-dom";
export const HeaderS = ({ type }) => {
return (
<div className="px-10 flex-col justify-center">
<div className="text-3xl font-extrabold">
{type === "signup" ? "Create an Account" : "Log in To your Account"}
</div>
<div className="text-slate-400 mb-4 ">
{type === "signup"
? "Already have a Account?"
: "Dont Have a Account yet?"}
<Link
className="pl-2 underline"
to={type === "signup" ? "/signin" : "/signup"}
>
{type === "signup" ? "Sign in" : "Signup"}
</Link>
</div>
</div>
);
};
//components/Auth.jsx
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import { BACKEND_URL } from "../config";
import { HeaderS } from "./header";
export const Auth = ({ type }) => {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const navigate = useNavigate();
async function sendRequest() {
try {
console.log(`${BACKEND_URL}/api/v1/user/${type}`);
const response = await axios
.post(`${BACKEND_URL}/api/v1/user/${type}`, {
username: username,
password: password,
});
console.log(response);
const jwt = response.data.token;
console.log(jwt);
localStorage.setItem("token", "Bearer " + jwt);
navigate("/blogs");
} catch (e) {
alert(
"Error, Try again| use a diffrent username |
make sure username is atleast 3 characters
long and password 8 characters "
);
}
}
return (
<div className="h-screen flex justify-center flex-col">
<div className="flex justify-center">
<div>
<HeaderS type={type}></HeaderS>
<LabelledInput
label="Username"
placeholder="Alucard"
onChange={(e) => {
setUsername(e.target.value);
}}
/>
<LabelledInput
label="Password"
type={"password"}
placeholder="Apassword"
onChange={(e) => {
setPassword(e.target.value);
}}
/>
<button
onClick={sendRequest}
type="button"
className="mt-8 w-full text-center text-white
bg-gray-800 hover:bg-gray-900 focus:outline-none
focus:ring-4 focus:ring-gray-300 font-medium
rounded-lg text-sm px-5 py-2.5 me-2 mb-2"
>
{type === "signup" ? "SignUp" : "Sign in"}
</button>
</div>
</div>
</div>
);
};
function LabelledInput({ label, placeholder,
onChange, type = "text" }) {
return (
<div className="mt-3 ">
<label className="block mb-2 text-sm
font-medium text-gray-900">
{label}
</label>
<input
onChange={onChange}
type={type}
className="bg-gray-50 border border-gray-300
text-gray-900 text-sm rounded-lg focus:ring-blue-500
focus:border-blue-500 block w-full p-2.5"
placeholder={placeholder}
/>
</div>
);
}
//components/Quote.jsx
export const Quote = () => {
return (
<div className="bg-slate-200 h-screen flex justify-center flex-col">
<div className="flex justify-center">
<div className="max-w-md ">
<div className="text-3xl font-bold">A great Bloging site</div>
<div className="text-xl font-semibold flex text-left mt-2">
LuffyToro
</div>
<div className="text-m font-sm text-slate-400 flex text-left">
CEO Akifena
</div>
</div>
</div>
</div>
);
};
//pages/signup.jsx
import { Auth } from "../components/Auth"
import { Quote } from "../components/Quote"
export const Signup = () => {
return <div>
<div className="grid lg:grid-cols-2">
<div>
<Auth type="signup" />
</div>
<div className="hidden lg:block">
<Quote></Quote>
</div>
</div>
</div>
}
//pages/signin.jsx
import { Auth } from "../components/Auth"
import { Quote } from "../components/Quote"
export const Signin = () => {
return <div>
<div className="grid lg:grid-cols-2">
<div>
<Auth type="signin" />
</div>
<div className="hidden lg:block">
<Quote></Quote>
</div>
</div>
</div>
}
// frontend/App.jsx
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { Signin } from './pages/signin'
import { Signup } from './pages/signup'
function App() {
return (
<>
<BrowserRouter>
<Routes>
<Route path="/signup" element={<Signup />} />
<Route path="/signin" element={<Signin />} />
<Route path="" element={<Signin />}></Route>
</Routes>
</BrowserRouter>
</>
)
}
export default App
//components/Blogcard.jsx
import { Link } from "react-router-dom";
export const Blogcard = ({ id, authorName, title,
content, publishedDate }) => {
return (
<Link to={`/blog/${id}`}>
<div className="border-b border-slate-200 p-4
w-screen max-w-screen-md cursor-pointer">
<div className="flex w-full">
<div className="flex justify-center flex-col">
<Avatar name={authorName} />
</div>
<div className="font-extralight pl-2
flex justify-center flex-col">
{authorName}
</div>
<div className="flex justify-center flex-col pl-2">
<Circle></Circle>
</div>
<div className="pl-2 font-thin text-slate-500
flex justify-center flex-col">
{publishedDate}
</div>
</div>
<div className="pt-3 text-xl font-semibold">{title}</div>
<div className="pt-1 text-md font-thin">
{convHtml(content).slice(0, 100) + "..."}
</div>
<div className="text-slate-400 test-sm font-thin pt-4">
{`${Math.ceil(content.length / 500)}
minute(s) read`}
</div>
</div>
</Link>
);
};
export function Circle() {
return <div className="bg-slate-400 w-1 h-1 rounded-full"></div>;
}
export function Avatar({ name }) {
return (
<div className="relative inline-flex items-center justify-center
w-7 h-7 overflow-hidden bg-gray-100 rounded-full dark:bg-gray-600">
<span className="font-medium text-gray-600 dark:text-gray-300">
{name[0]}
</span>
</div>
);
}
function convHtml(html) {
const plainText = html.replace(/<\/?[^>]+(>|$)/g, "");
return plainText;
}
// frontend/src/components/Appbar.jsx
import { Link } from "react-router-dom"
import { Avatar } from "./Blogcard"
export const AppBar = () => {
return <div className="py-2 border-b flex
justify-between px-10">
<Link to={'/blogs'}>
<div className="pt-2">
Medium
</div>
</Link>
<div >
<Link to={'/publish'}>
<button type="button" className="text-white
bg-green-700 hover:bg-green-800 focus:outline-none
focus:ring-4 focus:ring-green-300 font-medium
rounded-full text-sm px-5 py-2.5 text-center
me-5" >Publish</button>
</Link>
<Avatar name={localStorage.getItem("Username")}>
</Avatar>
</div>
</div>
}
//components/BlogPage.jsx
import { AppBar } from "./Appbar"
import { Avatar } from "./Blogcard"
import parse from 'html-react-parser'
export const BlogPage = ({ blog }) => {
const RederedC = parse(blog.content)
console.log(RederedC)
return (<div>
<AppBar name={blog.author.Username} />
<div className="grid grid-cols-12 px-10 w-full
pt-12 max-w-screen-2xl">
<div className=" col-span-8">
<div className="text-5xl font-extrabold">
{blog.title}
</div>
<div className="text-slate-500 pt-2">
Posted on 2nd Dec 2032
</div>
<div className="pt-4">
{RederedC}
</div>
</div>
<div className="col-span-4">
<div className="text-slate-600">
Author
</div>
<div className="flex">
<div className="pr-2 flex justify-center flex-col">
<Avatar name={blog.author.Username || "Anon"}></Avatar>
</div>
<div>
<div className="text-xl font-bold">
{blog.author.Username || "Anonymous"}
</div>
</div>
</div>
</div>
</div>
</div>
)
}
//components/tinytext.jsx
import React, { useRef, useState } from "react";
import { Editor } from "@tinymce/tinymce-react";
import { BACKEND_URL } from "../config";
import axios from "axios";
import { useNavigate } from "react-router-dom";
export function Editortiny() {
const editorRef = useRef(null);
const [title, stitle] = useState("");
let ctemp = "";
const navigate = useNavigate();
async function sendRequest() {
try {
const lk = `${BACKEND_URL}/api/v1/blog/post`;
if (ctemp === "") {
console.log("input empty");
return;
}
const response = await axios.post(
lk,
{
title: title,
content: ctemp,
},
{
headers: {
Authorization: localStorage.getItem("token"),
},
}
);
navigate(`/blog/${response.data.id}`);
} catch (e) { }
}
const savedata = async () => {
if (editorRef.current) {
ctemp = await editorRef.current.getContent();
sendRequest();
} else {
return null;
}
};
return (
<div>
<div className="flex justify-center w-full pt-8">
<div className="max-w-screen-lg w-full me-20">
<input
onChange={(e) => {
stitle(e.target.value);
}}
className="m-5 bg-gray-50 border border-gray-300
text-gray-900 text-sm rounded-lg
focus:ring-blue-500 focus:border-blue-500
block w-full p-2.5"
placeholder="Title"
/>
</div>
</div>
<div className="p-6">
<Editor
apiKey="YOUR_API_KEY"
onInit={(evt, editor) =>
(editorRef.current = editor)}
initialValue="<p>This is the initial content
of the editor.</p>"
init={{
height: 750,
menubar: false,
plugins: [
"advlist",
"autolink",
"lists",
"link",
"image",
"charmap",
"preview",
"anchor",
"searchreplace",
"visualblocks",
"code",
"fullscreen",
"insertdatetime",
"media",
"table",
"code",
"help",
"wordcount",
],
toolbar:
"undo redo | blocks | " +
"bold italic forecolor | alignleft aligncenter " +
"alignright alignjustify | bullist numlist outdent indent | " +
"removeformat | help",
content_style:
"body { font-family:Helvetica,Arial,sans-serif; font-size:14px }",
}}
/>
<div className="flex justify-center flex-col pt-5">
<button
onClick={savedata}
type="button"
className="text-white bg-blue-700 hover:bg-blue-800
focus:outline-none focus:ring-4 focus:ring-blue-300
font-medium rounded-full text-sm px-5 py-2.5
text-center me-2 mb-2"
>
Submit
</button>
</div>
</div>
</div>
);
}
//hooks/index.js
import { useEffect, useState } from "react";
import { BACKEND_URL } from "../config";
import axios from "axios";
export const useBlog = ({ id }) => {
const [loading, setLoading] = useState(true);
const [blog, setBlog] = useState(null);
useEffect(() => {
axios
.get(`${BACKEND_URL}/api/v1/blog/${id}`, {
headers: {
Authorization: localStorage.getItem("token"),
},
})
.then((response) => {
setBlog(response.data);
setLoading(false);
});
}, [id]);
getuname();
return {
loading,
blog,
};
};
export const useBlogs = () => {
const [loading, setLoading] = useState(true);
const [blogs, setBlogs] = useState([]);
useEffect(() => {
axios
.get(`${BACKEND_URL}/api/v1/blog/bulk`, {
headers: {
Authorization: localStorage.getItem("token"),
},
})
.then((response) => {
setBlogs(response.data.blogs);
setLoading(false);
});
}, []);
getuname();
return {
loading,
blogs,
};
};
const getuname = () => {
useEffect(() => {
axios
.get(`${BACKEND_URL}/api/v1/blog/uname`, {
headers: {
Authorization: localStorage.getItem("token"),
},
})
.then((response) => {
localStorage.setItem("Username", response.data.uname);
});
}, []);
};
//pages/Blog.jsx
import { useParams } from "react-router-dom";
import { useBlog } from "../hooks";
import { BlogPage } from "../components/BlogPage";
export const Blog = () => {
const { id } = useParams();
const { loading, blog } = useBlog({
id: id || ""
});
if (loading) {
return <div>
loading...
</div>
}
return <div>
<BlogPage blog={blog} />
</div>
}
//pages/Blogs.jsx
import { AppBar } from "../components/Appbar"
import { Blogcard } from "../components/Blogcard"
import { useBlogs } from "../hooks";
export const Blogs = () => {
const { loading, blogs } = useBlogs();
if (loading) {
return <div>
Loading...
</div>
}
return (<div>
<AppBar />
<div className="flex justify-center">
<div className="max-w-xl ">
{blogs.map(blog => <Blogcard
authorName={blog.author.Username || "Anon"}
title={blog.title}
publishedDate={blog.createdAt.substring(0, 10)}
content={blog.content}
id={blog._id}>
</Blogcard>)}
</div>
</div>
</div>
)
}
//pages/publish.jsx
import { AppBar } from "../components/Appbar"
import { Editortiny } from "../components/tinytext"
export const Publish = () => {
return <div>
<AppBar />
<Editortiny />
</div>
}
//App.jsx
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { Signin } from './pages/signin'
import { Blog } from './pages/Blog'
import { Blogs } from './pages/Blogs'
import { Signup } from './pages/signup'
import { Publish } from './pages/publish'
function App() {
return (
<>
<BrowserRouter>
<Routes>
<Route path="/signup" element={<Signup />} />
<Route path="/signin" element={<Signin />} />
<Route path="/blog/:id" element={<Blog />} />
<Route path="/blogs" element={<Blogs />} />
<Route path="/publish" element={<Publish />}></Route>
<Route path="" element={<Signin />}></Route>
</Routes>
</BrowserRouter>
</>
)
}
export default App
Step 11. For the next step we will be using tinymce library, and you will need a API key to use there text editor
you can get one for free using the following steps:
- go to https://www.tiny.cloud/
- Log in or Signup to your account
- Go To https://www.tiny.cloud/my-account/integrate/#react to get your API Key.
To start the frontend run the following command
npm run dev