Table of Contents
If you want to take a direct look at this project, you can do so right here.
This project was built following this tutorial.
Sociopedia is a social media web app, built using the MERN Stack (MongoDB, Express.js, React, Node.js).
On the backend, we have some basic CRUD operations: creating and deleting a user account, logging in and out, reading and updating user profiles, and creating, reading, updating posts. The connection to the database is set up using Mongoose, passwords are encrypted using Bcrypt, the authentication and authorization is delt with using JSON Web Token, images are uploaded using Multer and are stored in an AWS S3 bucket.
On the frontend, we use React Router for navigation as this is a SPA, we use Redux Toolkit for global state management, Axios as HTTP client for API requests, Formik and yup for form management and validation, and React Dropzone to collect images upload. The styling is done using Emotion.
This project is hosted on Render and utilizes their power-efficient servers, which spin down after periods of inactivity. As a result, you may experience a slightly longer loading time as the server spins back up for use.
- Tech stack :
- Tools :
Let me present to you the folder structure of this project, with its main components.
In the root folder, we find the README.md
, the LICENSE
, the package.json
for the build and start scripts, and the .gitignore
files.
We also find the public/assets/
folder with the images used for this README.md
file.
The rest of the project is divided in the two folders: the server/
folder for the backend of the app, and the client/
folder for the frontend.
In the server/
folder, the most important files are the index.js
file where the bases of the server is set up, the package.json
file with all the packages needed to run the backend, and the .env.example
file used as a base to set up the .env
file for replicating this project.
Then, in server/
, we find multiple folders :
controllers/
with the auth.js
, posts.js
and users.js
files, which are the controllers used in the routes of the same name, and the aws.js
file which defines the function used to delete an image from the AWS S3 bucket.
middleware/
with the auth.js
file with verifyToken middleware.
models/
with the Post.js
and User.js
files which set up their respective mongoose models.
public/
with the assets
folder in it where we store the image for the ad.
And routes/
with the auth.js
, posts.js
and users.js
files, which set up their respective routes.
Now for the client/
folder, the most important files are the package.json
file once again for the packages, and .env.example
used also to set up the .env
file.
Next, in client/
, there is also a public/
folder with the index.html
file on which the app sits on, and the assets/
folder with the different icons used in the project.
Then, still in client/
, there's the src/
folder where most of the frontend finds its place.
In src/
, we find index.js
which sets up React, App.js
that sets up the routes, theme.js
with the light and dark themes, and index.css
with a few global styles.
The rest of src/
is divided in three folders: components/
, scenes/
and state/
.
components/
has small components reused multiple times in the project, and state/
simply as a file which sets up the global state management using Redux toolkit and Axios.
scenes/
has the main elements of the project, each in their respective folders which are homePage/
, loginPage/
, navbar/
, profilePage/
and widgets/
. In the widgets/
folder, we find all the different panels used to populate the main pages of the app.
That's it for the folder structure !
If you want to clone this project, you must have Node.js installed, as well as Git.
-
To clone this project, first go in the directory you want to install the project in :
cd path/to/your/directory
-
And then run this command to clone the project :
git clone https://github.com/GWartelle/SocioPedia.git
-
For the backend, go in the server folder, and start by running the command
npm install
to install all the packages :cd server npm install
-
Next, still in the server folder, use the editor of your choice, and open the
.env.example
file. For example, using VSCode, you can run this command :code .env.example
-
Update the environment variables with your own :
MONGO_URL=... JWT_SECRET=... PORT=... NODE_ENV=... AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_REGION=... AWS_BUCKET_NAME=...
For the
MONGO_URL
, you can create your own free database on MongoDB.For
JWT_SECRET
, a random string of characters will do the trick. To easily create one, open a bash terminal and run this commmand :openssl rand -base64 32
For the
PORT
, you can choose whichever one you prefer.For
NODE_ENV
, leave it asdevelopment
unless you plan to deploy the app, in which case you should change it toproduction
.For
AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
,AWS_REGION
, andAWS_BUCKET_NAME
you can create a free AWS S3 bucket on AWS. -
Once it's done, change the name of the file to
.env
:mv .env.example .env
-
Lastly, to start the server using nodemon, run this command :
npm run start
-
For the frontend, you must go in the client folder and run the command
npm install
:cd ../client npm install
-
Next, still in the client folder, once again open the
.env.example
file in your editor :code .env.example
-
Update the environment variables with your own :
REACT_APP_API_URL=...
This variable is simply used to store the address of your server. So if you only intend to use this project on your local machine, you can leave it as
http://localhost:PORT
, just making sure that PORT matches the port you choose in your backend.env
file. But if you wish to deploy this app, you'll have to put the URL of your server inREACT_APP_API_URL
. -
Once it's done, change the name of the file to
.env
:mv .env.example .env
-
Finally, to run the React app and open it automatically in your browser, run this command :
npm run start
You will land on the login page.
Here you can obviously log in, using your email and your password.
But if you don't have an account yet, you can click on Don't have an account? Sign Up here.
to be redirected to the register page.
Here you will be asked to enter your first name, your last name, your location, your occupation, put your profile picture in the dropzone, enter your email, and finally your password.
If you already created an accoount you can just click on Already have an account? Login here.
.
Once registered you will be redirected back to the login page.
Once logged in, you'll land on the home page. To make things clear, this web app has a lot of mockup features. This is intentional as this leaves the opportunity to implement a lot of new things once the tutorial is finished.
So, now that this is out of the way, let's dive into this home page. At the top of the screen we have the navigation bar, with a search bar on the left. This is the first mockup feature, as you can enter some text into the search bar, but clicking the search icon won't do anything. This would be the opportunity to implement a profile searching feature.
On the right side we have the name of the user in a select menu, with the options of logging out and deleting the account. Besides that, the message, notification, and help icons are also mockup features, open for implementation. But the sun icon does work, as it toggles the dark mode on.
On the main page we found mutiple panels.
On the left, there is the User
panel, with all your info.
Here there are some mockup features too: the account parameters icon and the social profiles are here for demonstration sake.
And the views and impressions numbers are randomly generated at the creation of your profile.
In the center of the screen, we find the feed, with the MyPost
panel at the top.
This panel allows you to enter a message to post on the feed for all the other users to see.
You also have the possibility to send an image along with your message, by cliking on the image option, which pops off a dropzone where you can drag your image in.
As for the other options (Clip, Attachment and Audio), those are mockup features as well.
On the feed of Sociopedia, you can see all the other users' posts. On the top right of every posts (except yours), there is an add-friend icon which adds the creator of this post to your friend list. At the bottom of each post panel you have a heart icon for liking the post, and a comment icon for opening the comment section. For now you can only find mockup comments in this section, as the comment feature is not implemented yet.
On the right of the screen there's an Advertisement
panel, which is also a mockup feature.
And finally under this panel, you have the FriendList
panel, with all of your friends, with a remove-friend icon on their right if you want to remove them from the list.
Speaking of friends, if you click on a user's name, you are redirected to his/her profile page.
On the top left of this page, you can find the User
panel once again, but with the info of the user you cliked on this time.
Under this panel, there is also the FriendList
panel, but only with the friends of this user.
On the center of the screen, there is also a feed, but this one only contains the posts of the user of this profile page.
Lastly, if you want to get back to the home page, you can simply click on the Sociopedia
logo in the top left of the screen. And if you want to log out (or delete your account), you can do so by clicking the select menu with your username in the top right of the screen.
Compared to the original project, I changed some things here and there: I added a delete user account feature, I removed the possibility to add yourself to your own friend list, I also removed the possibility to create a post from the profile page of a user, etc ...
But one major thing I added to this project is the storage of uploaded images in an AWS S3 bucket, as this was mandatory if I wanted to deploy this project on Render.
So I wanted to share with you how I managed to implement this cloud storage. Of course the first part was to create an AWS account, create a new S3 bucket, and put all its credentials in my .env file.
Once this was done, it was time to set this up on my server :
/* AWS SDK SET UP */
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
/* FILE STORAGE */
const upload = multer({
storage: multerS3({
s3: s3,
bucket: process.env.AWS_BUCKET_NAME,
contentType: multerS3.AUTO_CONTENT_TYPE,
key: function (req, file, cb) {
const filename = file.originalname
.replace(/\s+/g, "-")
.replace(/[^\w.-]+/g, "");
cb(null, Date.now().toString() + "-" + filename);
},
}),
});
/* ROUTES WITH FILES */
app.post("/auth/register", upload.single("picture"), register);
app.post("/posts", verifyToken, upload.single("picture"), createPost);
The first thing to do was to create a S3Client, using the credentials of the bucket.
Next it was time to create the function upload
for uploading files in the bucket.
Multer, as this is the tool used for uploads in this project, uses the S3Client I just set up s3
, and connects with my bucket using the AWS_BUCKET_NAME
stored in my .env file.
And then Multer creates a key
to identify the uploaded file, by combining the current date in Unix time with the name of the file without spaces or special characters.
This function is then called as a middleware in the register
and posts
routes, before the register
and createPost
controllers as they are the only ones with the image upload feature.
Talking about these controllers, here is how they look like :
/* REGISTER USER */
export const register = async (req, res) => {
try {
/* ... */
const picturePath = req.file.location;
const newUser = new User({
/* ... */
picturePath,
/* ... */
});
const savedUser = await newUser.save();
res.status(201).json(savedUser);
} catch (err) {
res.status(500).json({ error: err.message });
}
};
/* CREATE POST */
export const createPost = async (req, res) => {
try {
/* ... */
let picturePath;
if (req.file) {
picturePath = req.file.location;
}
const newPost = new Post({
/* ... */
picturePath,
/* ... */
});
await newPost.save();
const post = await Post.find();
res.status(201).json(post);
} catch (err) {
res.status(409).json({ message: err.message });
}
};
To simplify things here, I just kept the picturePath
field in these MongoDB documents.
As you can see, the register
and createPost
controllers are both getting the image URL with req.file.location
, which is then stored in the database.
But aside from storing uploaded images in this S3 bucket, I also had to implement how to delete them from the bucket if the user deletes his/her account.
const s3 = new S3Client({
region: process.env.AWS_REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
function getKeyFromUrl(url) {
const index = url.lastIndexOf("/");
return url.substring(index + 1);
}
async function deleteImageFromS3(imageUrl) {
try {
const imageKey = getKeyFromUrl(imageUrl);
const deleteParams = {
Bucket: process.env.AWS_BUCKET_NAME,
Key: imageKey,
};
const deleteCommand = new DeleteObjectCommand(deleteParams);
await s3.send(deleteCommand);
console.log(`Image with key ${imageKey} deleted from S3 bucket.`);
} catch (err) {
console.error(`Error deleting image with key ${imageKey}: ${err.message}`);
}
}
First I had to set a new S3Client, because this is in another file, as implementing this function directly in the main file of the server would have been too cumbersome.
Next I created a getKeyFromUrl
function that extracts the key of an image to delete based on its URL by removing everything before the last /
.
Then comes the deleteImageFromS3
function which takes the URL of an image, extracts its key using getKeyFromUrl
, sets up the parameters of the deletion with the name of the bucket and the key of the image, and then sends a DeleteObjectCommand
with these parameters.
The last thing to do is to use this deleteImageFromS3
function in the deleteUser
controller:
/* DELETE USER */
export const deleteUser = async (req, res) => {
try {
const userId = req.params.userId;
// Find the user in the database and get their profile picture URL
const user = await User.findById(userId);
const profilePictureUrl = user.picturePath;
// Delete the user's profile picture from the bucket
await deleteImageFromS3(profilePictureUrl);
// Get the user's posts and their image URLs
const posts = await Post.find({ userId });
const imageUrls = posts.map((post) => post.picturePath);
// Delete the posts images from the bucket
for (const imageUrl of imageUrls) {
if (imageUrl) {
await deleteImageFromS3(imageUrl);
}
}
// Delete the user's posts
await Post.deleteMany({ userId });
// Delete the user from other users' friend list
await User.updateMany({}, { $pull: { friends: userId } }, { multi: true });
// Delete the user from the database
await User.findByIdAndDelete(userId);
res.status(200).json({ msg: "User deleted successfully." });
} catch (err) {
res.status(500).json({ error: err.message });
}
};
After getting the userId
from the request, this controller first finds the user in the database, gets the URL of his/her profile picture and then deletes it with deleteImageFromS3
.
The next thing to do is to get all the posts this user made, and then map the image's URL of each of these posts to an imageUrls
variable.
After this, the controller loops over each imageUrl
in imageUrls
and deletes it from the bucket if it exists (as it is possible to create a post without an image), using deleteImageFromS3
once again.
Finally, the controller just needs to update the MongoDB database by deleting all the posts of the user, removing him/her from the friend list of all the other users, and then deleting his/her profile.
And this is how I implemented an AWS cloud storage for my web app !
As mentioned multiple times in the usage section, this project leaves room for a lot of features to be built. Therefore, if I had more time on my hands to improve this web app, here are the features I would implement :
- I'd start by implementing a password confirmation to the register page, and I'd also add a password retrieval feature, as these are staples of all modern apps.
- Really implementing the commenting feature is a must have. Being able to comment other users' posts is a crucial part of every other social medias.
- Then, I would upgrade the friend list feature. Instead of being able to add anyone as a friend, the user should first have to send a friend request.
- Even if it'd represent a lot of work, it would be great to implement the instant messaging system. Based on the improved friend list, users could send private messages to their friends.
- With the comments properly implemented, the improved friend list, and the private messages, a nice feature to complement all of this would be notifications. Users could know when one of their friends creates a new post, when someone comments one of their own posts and when they receive a new message.
- Finally, even if uploading images could be enough, it'd be nice to also have videos and audios uploads for posts.
After that, this project could benefit from a lot of other changes like being able to update the user account, having real counts of views and impressions, adding the date and time to posts, really implementing the search bar, having the option for users to change/delete their posts, or being able to create an account using google, apple or other credentials, but I think this should be enough to begin with.
If you want to see more of my work, I invite you to go check my portfolio.
You can also take a look at my other projects on my github.
And if you'd like to get in touch with me, feel free to reach out on LinkedIn.
As mentionned above, this project was made following this tutorial. So I would like to thank its creator for his amazing work. If you want to go check the github of his tutorial you can do so right here. Feel free to give him a star, as his work was well structured and his explanations clear and useful.
And of course I would like to thank you for taking the time to read through all this ! I wish you the best π
Have a great day π
Distributed under the MIT License. See opensource.org for more information.