DigitalOcean MongoDB Hackathon - Memories Sharing App

Hello everyone,
In this post we are going to learn how to create a memories sharing app on MERN stack. If you don't know what MERN stack is, it's a full stack technology which utilises the following:

  1. MongoDB - A NoSQL Database which is Document based
  2. ExpressJS - A microservice web framework.
  3. ReactJS - A frontend Library
  4. NodeJS - A runtime environment for JavaScript used on server side.

So let's get started and build our app.

We would be requiring:

  1. NodeJS
  2. Visual Studio Code or any other IDE of your choice.

First we need to create a project directory and initialize our project using the command:

npm init

Server:

This will create a package.json file. Now we need to add a few external dependencies to our app. So to do that run the following command:

npm i body-parser cors express mongoose

After installing these, its's time to create a Database instance on DigitalOcean Managed MongoDB platform.

Copy the connection string and add it to .env file with MongoURI as variable name.

Once we have created the .env to add a few scripts to our package.json file:

"scripts": {
    "start": "node server.js",
    "server": "nodemon server.js",
    "client": "npm start --prefix client",
    "client-install": "cd client && npm install",
    "dev": "concurrently -n server,client -c red,blue \"npm run server\" \"npm run client\"",
    "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client",
    "build": "npm run heroku-postbuild"
  },

After doing so the package.json file should look like this:

{
  "name": "memories-app",
  "version": "1.0.0",
  "description": "A memories app made on MERN stack",
  "main": "server.js",
  "type": "module",
  "scripts": {
    "start": "node server.js",
    "server": "nodemon server.js",
    "client": "npm start --prefix client",
    "client-install": "cd client && npm install",
    "dev": "concurrently -n server,client -c red,blue \"npm run server\" \"npm run client\"",
    "heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client",
    "build": "npm run heroku-postbuild"
  },
  "keywords": [],
  "author": "Somsubhra Das",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "dotenv": "^8.2.0",
    "express": "^4.17.1",
    "mongoose": "^5.10.13"
  }
}

Now Copy the following code to a new file server.js:

import express from "express";
import bodyParser from "body-parser";
import mongoose from "mongoose";
import cors from "cors";
import { config } from "dotenv";

import postRoutes from "./routes/post.js";

config();

const app = express();

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

app.use(cors());

const CONNECTION_URL = process.env.MongoURI;

mongoose
  .connect(CONNECTION_URL, {
    useNewUrlParser: true,
    useUnifiedTopology: true,
    useFindAndModify: false,
  })
  .then(() => console.log("Successfully connected to MongoDB"))
  .catch((err) => console.log(`Error connecting to MongoDB ${err.message}`));

// app.get("/", (req, res) => res.send("Hello"));

app.use("/posts", postRoutes);

// Serve static assets if it's production environment
if (process.env.NODE_ENV === "production") {
  // Set static folder
  app.use(express.static("client/build"));
  app.get("*", (req, res) => {
    res.sendFile(path.resolve(__dirname, "client", "build", "index.html"));
  });
}

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => console.log(`Server is running on port ${PORT}`));

The above code configures the database connections, routes, port.

After that create a models folder and inside it create postMessage.js file. Copy the following to the file:

import mongoose from "mongoose";

const postSchema = mongoose.Schema({
  title: String,
  message: String,
  creator: String,
  tags: [String],
  selectedFile: String,
  likeCount: {
    type: Number,
    default: 0,
  },
  createdAt: {
    type: Date,
    default: new Date(),
  },
});

const PostMessage = mongoose.model("PostMessage", postSchema);

export default PostMessage;

The above code creates a MongoDB Schema for our Database. It will be used for data fetching and data entry to database.

After that create a routes folder and inside it create a file named post.js and enter the following:

import express from "express";
import {
  getPosts,
  createPost,
  updatePost,
  deletePost,
  likePost,
} from "../controllers/posts.js";

const router = express.Router();

router.get("/", getPosts);
router.post("/", createPost);
router.patch("/:id", updatePost);
router.delete("/:id", deletePost);
router.patch("/:id/likePost", likePost);

export default router;

This setups the routes for our CRUD APIs. Having done so, it's time to create the controllers to handle our requests and responses. So create a folder named controllers and inside it create posts.js:

import PostMessage from "../models/postMessage.js";
import mongoose from "mongoose";

export const getPosts = async (req, res) => {
  try {
    const postMessages = await PostMessage.find();
    // console.log(postMessages);

    return res.json(postMessages);
  } catch (error) {
    res.status(404).json({ message: error.message });
  }
};

export const createPost = async (req, res) => {
  const post = req.body;
  // console.log(req.body);
  const newPost = new PostMessage(post);

  try {
    await newPost.save();

    res.status(201).json(newPost);
  } catch (error) {
    res.status(409).json({ message: error.message });
  }
};

export const updatePost = async (req, res) => {
  const { id: _id } = req.params;
  const post = req.body;

  if (!mongoose.Types.ObjectId.isValid(_id)) {
    return res.status(404).send("No post with that id");
  }

  const updatedPost = await PostMessage.findByIdAndUpdate(_id, post, {
    new: true,
  });

  return res.json(updatedPost);
};

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

  if (!mongoose.Types.ObjectId.isValid(_id)) {
    return res.status(404).send("No post with that id");
  }

  await PostMessage.findByIdAndRemove(_id);

  return res.json({ message: "Post deleted successfully" });
};

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

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

  const updatedPost = await PostMessage.findByIdAndUpdate(
    id,
    {
      $inc: { likeCount: 1 },
    },
    { new: true }
  );
  return res.json(updatedPost);
};

This code handles the request and responses for the Posts CRUD API server.

Having done so, we have successfully setup our server side and the APIs.

Client:

Next we would be handling our client side setup. So run the following command:

npx create-react-app client

This will create a directory named client with all the frontend code. Change directory to client and run:

npm i @material-ui/core @material-ui/icons axios moment react-redux redux redux-thunk

Now we need to setup actions. Create actions folder and inside it posts.js and enter the following code:

import {
  CREATE,
  UPDATE,
  DELETE,
  LIKE,
  FETCH_ALL,
} from "../constants/actionTypes";
import * as api from "../api";

// Action Creators
export const getPosts = () => async (dispatch) => {
  try {
    const { data } = await api.fetchPosts();
    // const action = { type: "FETCH_ALL", payload: [] };
    dispatch({ type: FETCH_ALL, payload: data });
  } catch (error) {
    console.log(error.message);
  }
};

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

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

export const updatePost = (id, post) => async (dispatch) => {
  try {
    const { data } = await api.updatePost(id, post);
    dispatch({ type: UPDATE, payload: data });
  } catch (error) {
    console.log(error.message);
  }
};

export const deletePost = (id) => async (dispatch) => {
  try {
    await api.deletePost(id);
    dispatch({ type: DELETE, payload: id });
  } catch (error) {
    console.log(error);
  }
};

export const likePost = (id) => async (dispatch) => {
  try {
    const { data } = await api.likePost(id);
    dispatch({ type: LIKE, payload: data });
  } catch (error) {
    console.log(error.message);
  }
};

Now it's time to create the API calls. Create api folder and inside it index.js:

import axios from "axios";

const url = "/posts";

export const fetchPosts = () => axios.get(url);

export const createPost = (newPost) => axios.post(url, newPost);

export const updatePost = (id, updatedPost) =>
  axios.patch(`${url}/${id}`, updatedPost);

export const deletePost = (id) => axios.delete(`${url}/${id}`);

export const likePost = (id) => axios.patch(`${url}/${id}/likePost`);

Create reducers folder and inside it create index.js & posts.js:

index.js
import { combineReducers } from "redux";

import posts from "./posts";

export default combineReducers({ posts });
posts.js
import {
  CREATE,
  FETCH_ALL,
  DELETE,
  LIKE,
  UPDATE,
} from "../constants/actionTypes";

export default (posts = [], action) => {
  switch (action.type) {
    case FETCH_ALL:
      return action.payload;
    case CREATE:
      return [...posts, action.payload];

    case UPDATE:
    case LIKE:
      return posts.map((post) =>
        post._id === action.payload._id ? action.payload : post
      );

    case DELETE:
      return posts.filter((post) => post._id !== action.payload);

    default:
      return posts;
  }
};

Create constants folder and inside it actionTypes.js:

export const CREATE = "CREATE";
export const UPDATE = "UPDATE";
export const DELETE = "DELETE";
export const FETCH_ALL = "FETCH_ALL";
export const LIKE = "LIKE";

Now go to src/index.js and edit the contents to make the file resemble the following:

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

import reducers from "./reducers";

import "./index.css";

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

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

Now visit here and copy the components and images folders to your src folder of your project.

Also copy app.js, index.css, style.js to your project directory as well.

Finally run:

npm run dev

The App should look like this:

I hope you liked how this memories app was made. Please check out the following links:

27