DEV Community

Cover image for Building a MERN stack simple blog site from absolute scratch πŸ”₯
Pramit Marattha for Aviyel Inc

Posted on • Edited on • Originally published at aviyel.com

Building a MERN stack simple blog site from absolute scratch πŸ”₯

Even a few years ago, web application development was not what it is today. Nowadays, there are so many options that the uninitiated are frequently perplexed as to what is best for them. This applies not only to the overall stack but also to the development tools; there are so many options. This blog tutorial asserts that the MERN stack is ideal for developing a full web application, and it walks the reader through the entire process in a very detailed manner.

So, what exactly is the MERN stack?

MERN stack

The MERN stack is a popular set of technologies for creating a modern Single Page Application (SPA). MERN stands for MongoDB, Express, React, and Node.js:

  • Node.js is a popular server-side framework that allows us to run JavaScript code on a web server.

  • Express is a Node.js web application framework that makes Node application development simpler and faster.

  • MongoDB is a NoSQL database that stores data persistently in the form of collections and documents.

  • React is a JavaScript frontend library for creating user interfaces.

In this blog tutorial, we will create a full-stack blog application that performs CRUD operations by utilizing the MERN stack. This blog tutorial should help you understand the fundamental operations of the MERN stack.

Here is our application's final sneak peek.

Demo

Setting up the backend

Create a two folder name client and server inside your project directory, then open it inside the Visual Studio Code or any code editor of your choice.

Making Directory

Client and Server directory

Now, we'll begin by configuring our backend with npm and installing necessary packages, followed by configuring a MongoDB database, configuring a server with Node and Express, designing a database schema to define our Blog, and configuring API routes to create, read, update, and delete blog data and information from the database. so, now navigate to your server's directory and run the code below from the command prompt.

npm init -y 
Enter fullscreen mode Exit fullscreen mode

Updating package.json

To install the dependencies, execute the following commands in the terminal.

npm install cors express dotenv mongoose nodemon body-parser
Enter fullscreen mode Exit fullscreen mode

Dependencies

Package Installed

The "package.json" file should look like this after the dependencies have been installed.

Package json

And also, remember to update the scripts and type as well.

Script and Types

Now, navigate to your server directory and create a server.js file within it.

Configuring server.js

  • Import express module.

  • Import bodyParser module

  • Import mongoose module

  • Import CORS module

  • Use express() to start our app.

//server.js
import express from "express";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";

const app = express();
Enter fullscreen mode Exit fullscreen mode

Now we can use all of the different methods on that app instance. First, let's do some general setup. We'll use app.use and simply pass the bodyParser and limit it by 20 to 50mb because we're sending some images that can be very large in size, and we'll also specify the extended to true and do the same thing with the bodyParser URL encoded and pass the same parameter and now we are also going to use the CORS and call it as a function.

//server.js
import express from "express";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";

const app = express();

app.use(bodyParser.json({ limit: '50mb', extended: true }))
app.use(bodyParser.urlencoded({ limit: '50mb', extended: true }))
app.use(cors());
Enter fullscreen mode Exit fullscreen mode

Now it's time to link our server application to the real database, so we'll utilize the MongoDB database, especially the MongoDB cloud Atlas version, which means we'll be hosting our database onto their cloud.

Setting up MongoDB cloud cluster

MongoDB is a document-oriented database that is open source and cross-platform. MongoDB is a NoSQL database that stores data in JSON-like documents with optional schemas. MongoDB is a database created and distributed by MongoDB Inc. under the provisions of the Server Side Public License.

Official MongoDB website
MongoDB cloud

Sign in to MongoDB
Sign In MongoDB

Create a Project
Create Projects

Adding members
Adding Members

Building a database
Building Database

Creating a cluster
Creating Cluster

Selecting a cloud service provider
Service Provider

Make a cluster and wait for the cluster to be built before proceeding (usually takes around 5 -10 minutes)
Database Deployment

Navigate to the network access tab and select "Add IP address."
Allow IP address

In the database, create a user. You'll need the username and password for the MongoDB URI and finally, create a database user.
Creating User

Now, select the Choose a connection method.
Connection Method

Connect your application by clicking on it and finally select the correct driver and version.
Connect to cluster
Cloud Atlas

Now, inside server.js create a new variable and name it DB_CONNECTION. Inside it, create a string and simply paste the copied mongo DB connection URL. Now, inside it, enter your username and password, making sure to remove all the brackets and enter your own credentials. We'll secure the credential later by creating environmental variables, but for now, let's add it this way. The second thing we need is a PORT, so simply enter the port number, for now, 6000, and finally, we will use mongoose to connect to our database, so enter mongoose. connect() which is a function with two different parameters. The first will be the DB CONNECTION, and the second will be an object with two different options. The first is useNewUrlParser, which we will set to true, and the second is useUnifiedTopology, which we will also set to true. These objects are not required, but we will see some errors or warnings on our console. Following that, let's chain a.then() and.catch() because this will return a promise, so inside .then() will call the app and invoke listen, which has two parameters, the first of which is PORT and the second of which is the callback function that will be executed if our application is successfully connected and finally, if the connection to the database is not successful we will simply console log our error message.

//server.js
import express from "express";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";
import dotenv from "dotenv";

dotenv.config();

const app = express();

app.use(bodyParser.json({ limit: "50mb", extended: true }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
app.use(cors());

const DB_CONNECTION = process.env.DATABASE_URL;
const PORT = process.env.PORT || 6000;

mongoose
  .connect(DB_CONNECTION, { useNewUrlParser: true, useUnifiedTopology: true })
  .then(() =>
    app.listen(PORT, () =>
      console.log(`Server is running @ : http://localhost:${PORT}`)
    )
  )
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

Insert mongodb+srv into the .env file.

PORT=4000
DATABASE_URL=mongodb+srv://admin:<password>@cluster0.ddtsa.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
Enter fullscreen mode Exit fullscreen mode

That's it; we've successfully linked our server to the database.

Now that we've successfully connected to our database, let's get started on creating our routes for our backend application. To do so, we'll need to create a new folder inside the server called routes Within the routes folder, we will create a js file called blogPosts.routes.js.

This is what your folder structure should look like.

Folder Structure

We are going to add all of the routes inside of blogPosts.routes.js, so first we must import express from "express" and also configure our router. Now we can begin adding our routes to it.

// routes/blogPosts.routes.js
import express from "express";

const router = express.Router();

router.get("/", (req, res) => {
  res.send("Awesome MERN BLOG");
});

export default router;
Enter fullscreen mode Exit fullscreen mode

Let's get started on your server. js file and import the blogPost route. Now we can use express middleware to connect this blogPost to our application

// server.js
import express from "express";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";
import dotenv from "dotenv";

import blogPosts from "./routes/blogPosts.js";

dotenv.config();

const app = express();

app.use(bodyParser.json({ limit: "50mb", extended: true }));
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
app.use(cors());

// remember to add this after cors
app.use("/api/blogs", blogPosts);

const DB_CONNECTION = process.env.DATABASE_URL;
const PORT = process.env.PORT || 6000;

mongoose
  .connect(DB_CONNECTION, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
  })
  .then(() =>
    app.listen(PORT, () =>
      console.log(`Server is running at: http://localhost:${PORT}`)
    )
  )
  .catch((error) => console.log(error));
Enter fullscreen mode Exit fullscreen mode

Before we go any further, let's create a folder structure for our backend applications that will allow them to be much more scalable. So let’s create a new folder called controllers inside the controllers’ folder we are also going to create a file called blogPosts.controller.js .So controllers is simply a file that contains route-specific logic.
so your blogPosts.routes.jsand blogPosts.controller.jsshould resemble something like this.

Folder Structure

//routes/blogPosts.routes.js
import express from 'express';

import { getAllBlogPosts } from '../controllers/blogPosts.controller.js';

const router = express.Router();

router.get('/', getAllBlogPosts);

export default router;
Enter fullscreen mode Exit fullscreen mode

blogPosts.controller.js

//controllers/blogPosts.controller.js
import express from "express";
import mongoose from "mongoose";

const router = express.Router();

export const getAllBlogPosts = (req, res) => {
  res.send("Awesome MERN BLOG");
};

export default router;

Enter fullscreen mode Exit fullscreen mode

Let's make a new model for our blog posts, so make a folder called models and a file called blogs.js inside it.

The folder structure should resemble something like this

Folder Structure

// models/blogs.js
import mongoose from "mongoose";

const blogSchema = mongoose.Schema({
  title: String,
  description: String,
  tags: [String],
  fileUpload: String,
  upvote: {
    type: Number,
    default: 0,
  },
  creator: String,
  createdAt: {
    type: Date,
    default: new Date(),
  },
});

var BlogPost = mongoose.model("BlogArticle", blogSchema);

export default BlogPost;
Enter fullscreen mode Exit fullscreen mode

Let's start adding more routes now that our model is complete.

// routes/blogPosts.routes.js
import express from "express";

import {
  getAllBlogPosts,
  addBlogPost,
  getSinglePost,
  updateSingleBlogPost,
  removeSingleBlogPost,
  likeBlogPost,
} from "../controllers/blogPosts.controller.js";

const router = express.Router();

router.get("/", getAllBlogPosts);
router.post("/", addBlogPost);
router.get("/:id", getSinglePost);
router.patch("/:id", updateSingleBlogPost);
router.delete("/:id", removeSingleBlogPost);
router.patch("/:id/likeedBlogPost", likeBlogPost);

export default router;
Enter fullscreen mode Exit fullscreen mode

Now, inside the controller's folder, add the following code to your blogPosts.controllers.js file.

getAllBlogPosts method fetches all the blogs information.

export const getAllBlogPosts = async (req, res) => {
  try {
    const blogPosts = await BlogPost.find();
    res.status(200).json(blogPosts);
  } catch (error) {
    res.status(404).json({ message: error.message });
  }
};
Enter fullscreen mode Exit fullscreen mode

addBlogPost method adds/insert only one blog

export const addBlogPost = async (req, res) => {
  const { title, description, fileUpload, creator, tags } = req.body;

  const createNewPost = new BlogPost({
    title,
    description,
    fileUpload,
    creator,
    tags,
  });

  try {
    await createNewPost.save();
    res.status(201).json(createNewPost);
  } catch (error) {
    res.status(409).json({ message: error.message });
  }
};
Enter fullscreen mode Exit fullscreen mode

getSinglePost method fetches single blog post

export const getSinglePost = async (req, res) => {
  const { id } = req.params;

  try {
    const singlepost = await BlogPost.findById(id);

    res.status(200).json(singlepost);
  } catch (error) {
    res.status(404).json({ message: error.message });
  }
};

Enter fullscreen mode Exit fullscreen mode

updateSingleBlogPost method updates single blog posts

export const updateSingleBlogPost = async (req, res) => {
  const { id } = req.params;
  const { title, description, creator, fileUpload, tags } = req.body;

  if (!mongoose.Types.ObjectId.isValid(id))
    return res.status(404).send(`post ${id} not found`);

  const updatedBlogPost = {
    creator,
    title,
    description,
    tags,
    fileUpload,
    _id: id,
  };
  await BlogPost.findByIdAndUpdate(id, updatedBlogPost, { new: true });
  res.json(updatedBlogPost);
};
Enter fullscreen mode Exit fullscreen mode

removeSingleBlogPost method deletes single blog posts

export const removeSingleBlogPost = (req, res) => {
  const { id } = req.params;

  if (!mongoose.Types.ObjectId.isValid(id))
    return res.status(404).send(`post ${id} not found`);

  await BlogPost.findByIdAndRemove(id);

  res.json({ message: "Successfully deleted" });
};
Enter fullscreen mode Exit fullscreen mode

likeBlogPost method upvotes the posts

export const likeBlogPost = async (req, res) => {
  const { id } = req.params;

  if (!mongoose.Types.ObjectId.isValid(id))
    return res.status(404).send(`No post with id: ${id}`);

  const post = await BlogPost.findById(id);

  const updatedBlogPost = await BlogPost.findByIdAndUpdate(
    id,
    { upvote: post.upvote + 1 },
    { new: true }
  );

  res.json(updatedBlogPost);
};
Enter fullscreen mode Exit fullscreen mode

Your blogPosts.controller.js should resemble something like this

// blogPosts.controller.js
import express from "express";
import mongoose from "mongoose";

import BlogPost from "../models/blogs.js";

const router = express.Router();

export const getAllBlogPosts = async (req, res) => {
  try {
    const blogPosts = await BlogPost.find();
    res.status(200).json(blogPosts);
  } catch (error) {
    res.status(404).json({ message: error.message });
  }
};

export const addBlogPost = async (req, res) => {
  const { title, description, fileUpload, creator, tags } = req.body;

  const createNewPost = new BlogPost({
    title,
    description,
    fileUpload,
    creator,
    tags,
  });

  try {
    await createNewPost.save();
    res.status(201).json(createNewPost);
  } catch (error) {
    res.status(409).json({ message: error.message });
  }
};

export const getSinglePost = async (req, res) => {
  const { id } = req.params;

  try {
    const singlepost = await BlogPost.findById(id);

    res.status(200).json(singlepost);
  } catch (error) {
    res.status(404).json({ message: error.message });
  }
};

export const updateSingleBlogPost = async (req, res) => {
  const { id } = req.params;
  const { title, description, creator, fileUpload, tags } = req.body;

  if (!mongoose.Types.ObjectId.isValid(id))
    return res.status(404).send(`post ${id} not found`);

  const updatedBlogPost = {
    creator,
    title,
    description,
    tags,
    fileUpload,
    _id: id,
  };
  await BlogPost.findByIdAndUpdate(id, updatedBlogPost, { new: true });
  res.json(updatedBlogPost);
};

export const likeBlogPost = async (req, res) => {
  const { id } = req.params;

  if (!mongoose.Types.ObjectId.isValid(id))
    return res.status(404).send(`No post with id: ${id}`);

  const post = await BlogPost.findById(id);

  const updatedBlogPost = await BlogPost.findByIdAndUpdate(
    id,
    { upvote: post.upvote + 1 },
    { new: true }
  );

  res.json(updatedBlogPost);
};

export const removeSingleBlogPost = async (req, res) => {
  const { id } = req.params;

  if (!mongoose.Types.ObjectId.isValid(id))
    return res.status(404).send(`post ${id} not found`);

  await BlogPost.findByIdAndRemove(id);

  res.json({ message: "Successfully deleted" });
};

export default router;
Enter fullscreen mode Exit fullscreen mode

After restarting the server, you should see something similar to this:

Server Running


Configuring our Frontend

We'll start by setting up our frontend first using create-react-app .We will be creating the UI and its functionalities from the ground up. Let's get started on our application now.

Installing react application

Let us begin with the frontend part and craft it using react. So, if Node.js isn't already installed on your system, the first thing you should do is install it. So, go to the official Node.js website and install the correct and appropriate version. We need node js so that we can use the node package manager, also known as NPM.

Now, open client folder inside the code editor of your choice. For this tutorial, I will be using VScode. Next step, let’s open the integrated terminal and type npx create-react-app . this command will create the app inside the current directory and that application will be named as client

npx script

It usually takes only a few minutes to install. Normally, we would use npm to download packages into the project, but in this case, we are using npx, the package runner, which will download and configure everything for us so that we can start with an amazing template. It's now time to start our development server, so simply type npm start, and the browser will automatically open react-app.

Starting React Application

React boilerplate cleanup

Before we begin building our projects, we must first clean them up by removing some of the files provided by create-react-app. Your src files should look like this after you've cleaned them up.

Folder Structure

Installing some packages

We will need to install a few third-party packages for this project. so copy and paste the following command into your terminal

npm install @material-ui/core axios moment react-file-base64 redux react-redux redux-thunk
Enter fullscreen mode Exit fullscreen mode

After installing all these packages your packge.json file should look like this:

Package json

After we've installed all of our project's dependencies, let's add two components to it and call them Blogs, BlogPosts and BlogPostsForm.

Folder structure

Now that we've got everything set up, let's go over to our App.js file and start writing some code but before that lets create one Assets folder inside our src folder and add the logo image of yor choice. After that, make another folder called styles, and inside it, make a file called app.styles.js, and paste the following code inside it.

// src/styles/app.styles.js
import { makeStyles } from "@material-ui/core/styles";

export default makeStyles(() => ({
  navigationBar: {
    borderRadius: 10,
    margin: "6px 0px",
    display: "flex",
    flexDirection: "row",
    justifyContent: "center",
    alignItems: "center",
  },
  title: {
    color: "#8661d1",
    fontFamily: "Poppins",
    fontStyle: "bold",
  },
  image: {
    marginRight: "25px",
  },
}));
Enter fullscreen mode Exit fullscreen mode

Finally, go to App.js and import all of the necessary component files, styles, and components from the core material ui library, then implement it as follows.

//App.js
import React, { useState, useEffect } from "react";
import "./App.css";
import { Container, AppBar, Typography, Grow, Grid } from "@material-ui/core";
import blogLogo from "./Assets/blogLogo.gif";
import BlogPosts from "./components/BlogPosts";
import BlogPostsForm from "./components/BlogPostsForm";
import useStyles from "./styles/app.styles.js";

function App() {
  const appStyles = useStyles();

  return (
    <div className="App">
      <Container maxWidth="xl">
        <AppBar
          className={appStyles.navigationBar}
          position="static"
          color="inherit"
        >
          <img
            className={appStyles.image}
            src={blogLogo}
            alt="icon"
            height="100"
          />
          <Typography className={appStyles.title} variant="h4" align="center">
            Mern awesome blog
          </Typography>
        </AppBar>
        <Grow in>
          <Container>
            <Grid
              container
              justify="space-between"
              alignItems="stretch"
              spacing={2}
            >
              <Grid item xs={12} sm={7}>
                <BlogPostsForm />
              </Grid>
              <Grid item xs={12} sm={4}>
                <BlogPosts />
              </Grid>
            </Grid>
          </Container>
        </Grow>
      </Container>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now lets finally connect our frontend with the backend . so for that lets create a folder name api and inside it create a file name api.js . So let's import axios to make api calls, then specify our backend server url and write a function that simply fetches the post using axios.

import axios from "axios";

const url = "http://localhost:4000/api/blogs";

export const fetchAllBlogPosts = () => axios.get(url);
Enter fullscreen mode Exit fullscreen mode

Now, let's focus on adding redux functionality to our react application because all of our backend actions will be done with redux, so we need to dispatch those actions. To do that, let's create some files and folders to structure it so that our application can be scalable.So, inside our src folder, create a folder called actions as well as a folder called reducers, and inside both of those folders, create a file called blogPosts.js

Your folder structure should resemble something like this.

Folder structure

Before we proceed, let's fix our index.js file so that we can begin using redux from within it. Inside that file, let's import provider, which will keep track of the store, which is the global state, and which will allow us to access the store from anywhere within the application so that we don't have to be on the parent or even the child component so that we can easily access that state from anywhere, and after that, let's import createStore, applyMiddleware, and compose from the redux package, and finally, let's import thunk from redux-thunk and let's setup our index.js file accordingly.

//index.js
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import thunk from "redux-thunk";
import { reducers } from "./reducers/blogPosts.js";
import App from "./App";
import "./index.css";

const store = createStore(reducers, compose(applyMiddleware(thunk)));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);
Enter fullscreen mode Exit fullscreen mode

If you run your application now, you may encounter the module not found error.

Module not found error

Let's go to our reducer folder and fix that error, so let's create an index.js file inside it and import combineReducers from the redux package, export and call that combineReducers as a function, and put an object inside of it. Now we can imlement all of the individual reducers that we have in this application case, which is only going to have blogPosts.

// reducers/index.js
import { combineReducers } from "redux";
import blogPosts from "./blogPosts";

export const reducers = combineReducers({ blogPosts });
Enter fullscreen mode Exit fullscreen mode

Folder Structure

If everything went well, your application should be running smoothly at this point. We now need to dispatch our action within our main App.js file.

// App.js
import React, { useEffect } from "react";
import "./App.css";
import { Container, AppBar, Typography, Grow, Grid } from "@material-ui/core";
import blogLogo from "./Assets/blogLogo.gif";
import Blogs from "./components/Blogs";
import BlogPostsForm from "./components/BlogPostsForm";
import useStyles from "./styles/app.styles.js";
import { useDispatch } from "react-redux";
import { fetchAllBlogPosts } from "./actions/blogPosts";

function App() {
  const dispatch = useDispatch();
  const appStyles = useStyles();

  useEffect(() => {
    dispatch(fetchAllBlogPosts());
  }, [dispatch]);

  return (
    <div className="App">
      <Container maxWidth="xl">
        <AppBar
          className={appStyles.navigationBar}
          position="static"
          color="inherit"
        >
          <img
            className={appStyles.image}
            src={blogLogo}
            alt="icon"
            height="100"
          />
          <Typography className={appStyles.title} variant="h2" align="center">
            Mern awesome blog
          </Typography>
        </AppBar>
        <Grow in>
          <Grid
            container
            justifyContent="space-between"
            alignItems="stretch"
            spacing={2}
          >
            <Grid item xs={12} sm={3}>
              <BlogPostsForm />
            </Grid>
            <Grid item xs={12} sm={9}>
              <Blogs />
            </Grid>
          </Grid>
        </Grow>
      </Container>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Now, let's go over to our actions and import our api, and then create some Action creators, which are simply functions that return actions, so let's actually implement redux to pass or dispatch the function from data from our backend .

// actions/blogPosts.js
import * as api from "../api/api.js";

export const fetchAllBlogPosts = () => async (dispatch) => {
  try {
    const { data } = await api.fetchAllBlogPosts();

    dispatch({ type: GET_ALL_BLOGS, payload: data });
  } catch (error) {
    console.log(error.message);
  }
};
Enter fullscreen mode Exit fullscreen mode

Finally, let us return to our reducers and handle the logic of getting and fetching all of the blog posts.

// reducers/blogPosts.js
export default (posts = [], action) => {
  switch (action.type) {
    case "GET_ALL_BLOGS":
      return action.payload;
    default:
      return posts;
  }
};
Enter fullscreen mode Exit fullscreen mode

Now, let's actually retrieve these data from our child components, so let's go to our Blogs component and fetch the data from the global redux store we can do that by the help of useSelector

//components/Blogs
import React from "react";
import { Grid, CircularProgress } from "@material-ui/core";
import { useSelector } from "react-redux";

import BlogPosts from "../BlogPosts";
import useStyles from "./styles";

const Blogs = () => {
  const posts = useSelector((state) => state.blogPosts);
  const classes = useStyles();

  console.log("this is post", posts);

  return (
    <>
      <BlogPosts />
    </>
  );
};

export default Blogs;
Enter fullscreen mode Exit fullscreen mode

When you run your app, you may see an empty array and a network error; to fix this, simply include a proxy in your package.json file

proxy

So, if you still see that empty array, it means that the data was successfully fetched, and it is now time to implement the form so that we can make a post request to our database and actually add new posts to it.

So for that lets head over to the BlogPostsForm component and create a form. First step lets import all the component from material ui core library that we are going to implement in our form

// BlogPostsForm.js
import React, { useState, useEffect } from "react";
import { Paper, TextField, Typography, Button } from "@material-ui/core";
import { useDispatch, useSelector } from "react-redux";
import FileBase from "react-file-base64";

import useStyles from "./styles";
import { addBlogPosts, editBlogPosts } from "../../actions/blogPosts";

const BlogPostsForm = ({ blogPostId, setBlogPostId }) => {
  const [blogInfo, setBlogInfo] = useState({
    creator: "",
    title: "",
    description: "",
    tags: "",
    fileUpload: "",
  });
  const post = useSelector((state) =>
    blogPostId
      ? state.posts.find((message) => message._id === blogPostId)
      : null
  );
  const dispatch = useDispatch();
  const blogPostsStyles = useStyles();

  useEffect(() => {
    if (post) setBlogInfo(post);
  }, [post]);

  const handleSubmit = async (e) => {
    e.preventDefault();

    if (blogPostId === 0) {
      dispatch(addBlogPosts(blogInfo));
    } else {
      dispatch(editBlogPosts(blogInfo));
    }
  };

  return (
    <Paper className={blogPostsStyles.paper}>
      <form
        autoComplete="on"
        noValidate
        className={`${blogPostsStyles.root} ${blogPostsStyles.form}`}
        onSubmit={handleSubmit}
      >
        <Typography variant="h5">
          {blogPostId ? `Update "${post.title}"` : "✨ Create a blog ✨"}
        </Typography>

        <div className={blogPostsStyles.chooseFile}>
          <Typography> πŸ–ΌοΈ Upload Blog Image</Typography>
          <FileBase
            type="file"
            multiple={false}
            onDone={({ base64 }) =>
              setBlogInfo({ ...blogInfo, fileUpload: base64 })
            }
          />
        </div>
        <TextField
          name="title"
          variant="outlined"
          label="πŸ”₯ Blog Title"
          fullWidth
          value={blogInfo.title}
          onChange={(e) => setBlogInfo({ ...blogInfo, title: e.target.value })}
        />
        <TextField
          name="description"
          variant="outlined"
          label="πŸ“™ Blog Description"
          fullWidth
          multiline
          rows={7}
          value={blogInfo.description}
          onChange={(e) =>
            setBlogInfo({ ...blogInfo, description: e.target.value })
          }
        />
        <TextField
          name="creator"
          variant="outlined"
          label="✍️ Author name"
          fullWidth
          value={blogInfo.creator}
          onChange={(e) =>
            setBlogInfo({ ...blogInfo, creator: e.target.value })
          }
        />
        <Typography>Tags (5 max seperated by comma)</Typography>
        <TextField
          name="tags"
          variant="outlined"
          label="🏷️ Tags"
          fullWidth
          value={blogInfo.tags}
          onChange={(e) =>
            setBlogInfo({ ...blogInfo, tags: e.target.value.split(",") })
          }
        />

        <Button
          className={blogPostsStyles.publishButton}
          variant="contained"
          color="secondary"
          size="large"
          type="submit"
        >
          Publish πŸ“
        </Button>
      </form>
    </Paper>
  );
};

export default BlogPostsForm;
Enter fullscreen mode Exit fullscreen mode

Also, don't forget to modify the blogPostForm styles within styles. js

// components/BlogPostsForm/styles.js
import { makeStyles } from "@material-ui/core/styles";

export default makeStyles((theme) => ({
  root: {
    "& .MuiTextField-root": {
      margin: theme.spacing(1),
    },
  },
  paper: {
    padding: theme.spacing(5),
  },
  chooseFile: {
    width: "95%",
    margin: "10px 0",
  },
  publishButton: {
    marginBottom: 10,
  },
  form: {
    display: "flex",
    flexWrap: "wrap",
    justifyContent: "center",
  },
}));
Enter fullscreen mode Exit fullscreen mode

So before going any further lets fix our api first

// api/api.js
import axios from "axios";

const url = "http://localhost:4000/api/blogs";

export const fetchBlogPosts = () => axios.get(url);
export const addNewBlogPost = (newBlog) => axios.post(url, newBlog);
export const editSingleBlogPost = (id, editedBlogPost) =>
  axios.patch(`${url}/${id}`, editedBlogPost);
Enter fullscreen mode Exit fullscreen mode

After you've successfully added and exported the addNewBlogPost and editSingleBlogPost functions, let's actually implement them by creating some actions called addBlogPosts and editBlogPosts, respectively.

addBlogPosts action
add blog post

editBlogPosts action
edit blog post

Your blogPosts.js actions should look something like this.

// actions/blogPosts.js
import * as api from "../api/api.js";

export const fetchAllBlogPosts = () => async (dispatch) => {
  try {
    const { data } = await api.fetchBlogPosts();

    dispatch({ type: "GET_ALL_BLOG_POST", payload: data });
  } catch (error) {
    console.log(error.message);
  }
};

export const addBlogPosts = (post) => async (dispatch) => {
  try {
    const { data } = await api.addNewBlogPost(post);

    dispatch({ type: "ADD_NEW_BLOG_POST", payload: data });
  } catch (error) {
    console.log(error.message);
  }
};

export const editBlogPosts = (id, post) => async (dispatch) => {
  try {
    const { data } = await api.editSingleBlogPost(id, post);

    dispatch({ type: "EDIT_SINGLE_BLOG_POST", payload: data });
  } catch (error) {
    console.log(error.message);
  }
};
Enter fullscreen mode Exit fullscreen mode

After that, let's update the reducers section.

export default (posts = [], action) => {
    switch (action.type) {
        case "GET_ALL_BLOG_POST":
            return action.payload;
        case "ADD_NEW_BLOG_POST":
            return [...posts, action.payload];
        case "EDIT_SINGLE_BLOG_POST":
            return posts.map((post) =>
                post._id === action.payload._id ? action.payload : post
            );
        default:
            return posts;
    }
};
Enter fullscreen mode Exit fullscreen mode

Finally, let's update our App.js to include the blogPostId state, which we'll pass as a prop to our BlogPostsForm and Blogs components.

//App.js
import React, { useState, useEffect } from "react";
import "./App.css";
import { Container, AppBar, Typography, Grow, Grid } from "@material-ui/core";
import blogLogo from "./Assets/blogLogo.gif";
import Blogs from "./components/Blogs";
import BlogPostsForm from "./components/BlogPostsForm";
import useStyles from "./styles/app.styles.js";
import { useDispatch } from "react-redux";
import { fetchAllBlogPosts } from "./actions/blogPosts";

function App() {
  const [blogPostId, setBlogPostId] = useState(0);
  const dispatch = useDispatch();
  const appStyles = useStyles();

  useEffect(() => {
    dispatch(fetchAllBlogPosts());
  }, [blogPostId, dispatch]);

  return (
    <div className="App">
      <Container maxWidth="xl">
        <AppBar
          className={appStyles.navigationBar}
          position="static"
          color="inherit"
        >
          <img
            className={appStyles.image}
            src={blogLogo}
            alt="icon"
            height="100"
          />
          <Typography className={appStyles.title} variant="h2" align="center">
            Mern awesome blog
          </Typography>
        </AppBar>
        <Grow in>
          <Grid
            container
            justifyContent="space-between"
            alignItems="stretch"
            spacing={2}
          >
            <Grid item xs={12} sm={3}>
              <BlogPostsForm
                blogPostId={blogPostId}
                setBlogPostId={setBlogPostId}
              />
            </Grid>
            <Grid item xs={12} sm={9}>
              <Blogs setBlogPostId={setBlogPostId} />
            </Grid>
          </Grid>
        </Grow>
      </Container>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

After we've fixed App.js, we'll move on to our Blogs component and use the passed props within it and also drill it to the BlogPosts components

// components/Blogs.js
import React from "react";
import { Grid, CircularProgress } from "@material-ui/core";
import { useSelector } from "react-redux";

import BlogPosts from "../BlogPosts";
import useStyles from "./styles";

const Blogs = ({ setBlogPostId }) => {
  const posts = useSelector((state) => state.posts);
  const classes = useStyles();

  console.log("this is post", posts);

  return !posts.length ? (
    <CircularProgress />
  ) : (
    <Grid
      className={classes.container}
      container
      alignItems="stretch"
      spacing={4}
    >
      {posts.map((post) => (
        <Grid key={post._id} item xs={12} sm={12}>
          <BlogPosts post={post} setBlogPostId={setBlogPostId} />
        </Grid>
      ))}
    </Grid>
  );
};

export default Blogs;
Enter fullscreen mode Exit fullscreen mode

Now that we've completed almost everything, it's time to work on the individual blog posts. To do so, go to the BlogPosts components and install material UI icons first, then import several components from the material UI core library, and finally copy and paste the following code inside it.

// components/BlogPosts.js
import React from "react";
import {
  Typography,
  CardMedia,
  Button,
  Card,
  CardActions,
  CardContent,
} from "@material-ui/core/";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
import DeleteIcon from "@material-ui/icons/Delete";
import EditIcon from "@material-ui/icons/Edit";
import moment from "moment";
import { useDispatch } from "react-redux";
import blogImageLogo from "../../Assets/blogLogo.gif";

import { upvoteBlogPosts, removeBlogPosts } from "../../actions/blogPosts";
import useStyles from "./styles";

const BlogPosts = ({ post, setCurrentId }) => {
  const dispatch = useDispatch();
  const blogPostStyles = useStyles();

  return (
    <>
      <Card className={blogPostStyles.blogContainer}>
        <CardMedia
          className={blogPostStyles.imageContainer}
          image={post.fileUpload || blogImageLogo}
          title={post.title}
        />{" "}
        <div className={blogPostStyles.nameOverlay}>
          <Typography variant="h6"> {post.creator} </Typography>{" "}
          <Typography variant="body2">
            {" "}
            {moment(post.createdAt).fromNow()}{" "}
          </Typography>{" "}
        </div>{" "}
        <div className={blogPostStyles.editOverlay}>
          <Button
            style={{
              color: "white",
            }}
            size="small"
            onClick={() => setCurrentId(post._id)}
          >
            <EditIcon fontSize="default" />
          </Button>{" "}
        </div>{" "}
        <div className={blogPostStyles.tagSection}>
          <Typography variant="body2" color="textSecondary" component="h2">
            {" "}
            {post.tags.map((tag) => `#${tag} `)}{" "}
          </Typography>{" "}
        </div>{" "}
        <Typography
          className={blogPostStyles.titleSection}
          gutterBottom
          variant="h5"
          component="h2"
        >
          {post.title}{" "}
        </Typography>{" "}
        <CardContent>
          <Typography variant="body2" color="textSecondary" component="p">
            {" "}
            {post.description}{" "}
          </Typography>{" "}
        </CardContent>{" "}
        <CardActions className={blogPostStyles.cardActions}>
          <Button
            size="small"
            color="primary"
            onClick={() => dispatch(upvoteBlogPosts(post._id))}
          >
            <ArrowUpwardIcon fontSize="small" /> {post.likeCount}{" "}
          </Button>{" "}
          <Button
            size="small"
            color="primary"
            onClick={() => dispatch(removeBlogPosts(post._id))}
          >
            <DeleteIcon fontSize="big" />
          </Button>{" "}
        </CardActions>{" "}
      </Card>{" "}
    </>
  );
};

export default BlogPosts;
Enter fullscreen mode Exit fullscreen mode

Finally, let's create an action to actually upvote and remove the blog post. First, create a function inside API and name it upvoteSingleBlogPost and removeBlogPost, then export it.

// api/api.js
import axios from "axios";

const url = "http://localhost:4000/api/blogs";

export const fetchBlogPosts = () => axios.get(url);
export const addNewBlogPost = (newBlog) => axios.post(url, newBlog);
export const editSingleBlogPost = (id, editedBlogPost) =>
    axios.patch(`${url}/${id}`, editedBlogPost);
export const upvoteSingleBlogPost = (id) =>
    axios.patch(`${url}/${id}/likedBlogPost`);
export const removeBlogPost = (id) => axios.delete(`${url}/${id}`);
Enter fullscreen mode Exit fullscreen mode

Finally, let us fix our reducers and we will be finished with our application.

// reducers/blogPosts.js
export default (posts = [], action) => {
    switch (action.type) {
        case "GET_ALL_BLOG_POST":
            return action.payload;
        case "ADD_NEW_BLOG_POST":
            return [...posts, action.payload];
        case "EDIT_SINGLE_BLOG_POST":
            return posts.map((post) =>
                post._id === action.payload._id ? action.payload : post
            );
        case "UPVOTE_SINGLE_BLOG_POST":
            return posts.map((post) =>
                post._id === action.payload._id ? action.payload : post
            );
        case "DELETE_SINGLE_BLOG_POST":
            return posts.filter((post) => post._id !== action.payload);
        default:
            return posts;
    }
};
Enter fullscreen mode Exit fullscreen mode

We've gone over quite a lot of material to give you the knowledge you need to build a fully-fledged MERN stack application from absolute scratch.

The complete source code can be found here.

https://github.com/aviyeldevrel/devrel-tutorial-projects/tree/main/MERN-awesome-blog

Conclusion

In the first section of this blog tutorial, we built the back-end server using Node.js, Express, and MongoDB. We used the Mongoose library to connect the Node.js / Express server to MongoDB, and then in the second section of this tutorial, we created the React front-end application and also used redux for managing our states globally for our MERN stack blog application. Cheers !! Happy Coding !!

Main article available here => https://aviyel.com/post/1304

Happy Coding!!

Follow @aviyelHQ or sign-up on Aviyel for early access if you are a project maintainer, contributor, or just an Open Source enthusiast.

Join Aviyel's Discord => Aviyel's world

Twitter =>[https://twitter.com/AviyelHq]

Top comments (3)

Collapse
 
meo3w profile image
Phil Hasenkamp

Outstanding job well done on this! Thank you for sharing your knowledge πŸ™πŸΌπŸ™πŸΌπŸ™πŸΌ

Collapse
 
codefrombasics_434b222334 profile image
Codefrombasics

Great article on full-stack development! For those interested in mastering modern web development, I highly recommend checking out our MERN Stack course. It covers everything from MongoDB, Express.js, React, and Node.js, ensuring you build robust and scalable applications. Whether you're a beginner or looking to upskill, this course offers hands-on projects and expert guidance to help you succeed. MERN Stack Training in Chennai Happy coding!

Collapse
 
andrewbaisden profile image
Andrew Baisden

Very nice just one thing you don't need to import bodyParser anymore because its built in.