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:
- MongoDB - A NoSQL Database which is Document based
- ExpressJS - A microservice web framework.
- ReactJS - A frontend Library
- NodeJS - A runtime environment for JavaScript used on server side.
So let's get started and build our app.
We would be requiring:
- NodeJS
- 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: