Kristen Mazza
September 13, 2023
In order to sharpen my skills with the MERN stack (MongoDB, Express.js, React, Node.js), I decided to develop the blog that you're currently reading. In this post, I will walk you through my approach and experiences while building the blog.
I tackled the development of this blog by breaking it down into three parts:
An API-only backend with a database to store and manipulate blog data
A front-end to display the blog (where you are now)
A separate front-end content management system to create and edit blog posts (where I am now)
I already had a good sense of what I wanted the blog to look like and contain, so I decided to focus on the backend to start.
To structure the project, I used a Model-View-Controller (MVC) pattern so that there would be a clear separation of logic. This meant that I needed to create some database models to define the data structures that the blog should contain. I created three models – user, post, and comment – as shown below.
Once I finished defining what data to store using the models, I needed to decide what to do with the data. For the posts, I knew it was important to implement all of the CRUD (create, read, update, and delete) functions. This is where the controllers came in. The controllers stand between the data and the presentation of the data (the view). They control the logic that modifies the model and/or view based on the input from the users. For example, if I wanted to edit a blog post, the input would be sent to the controller, which manipulates the model, and then the updated data would be sent to the view. Express-validator was used to validate and sanitize the created or updated data.
I created API endpoints following REST principles, each linked to a corresponding controller function. For the posts, I set up endpoints to manage the CRUD functions: getting the blog post list, creating a post, retrieving a post’s details, updating a post, adding a main image to a post, and deleting a post. For the comments, I used endpoints for getting all comments, creating a comment, and deleting a comment. For the authentication, I implemented endpoints for signing up and logging in. Finally, I created an endpoint for getting a user’s details.
I chose to use the Passport JSON Web Tokens (JWT) strategy for authentication and authorization. Passport is a Node.js middleware that can use username and password, among other request strategies, as an authentication method. It stores the user in a session and creates a cookie. JWT removes this reliance on sessions and cookies. With JWT-based authentication, when a user logs in to a web application, the server can generate an encrypted token, which gets passed around in requests to help identify the logged-in user, particularly when accessing protected routes.
Using this approach, when a user logs in, the user is retrieved from the database, and the password received in the request is compared with the hashed one associated with the user in the database (secured using bcryptjs). If the username and password are correct, JWT creates a token using the user data, and the token is returned. JWT is where I hit my first snag. Although I was somewhat familiar with Passport from a previous project, I had not used JWT, so it took a good amount of research to understand how the two play together with the Passport JWT strategy.
One of my visions for the blog was to have main images associated with each post. For this, I needed a way to upload image files and a place to store the images. This was where I met my next challenge. In a recent project, I used multer (a node.js middleware) to handle the upload of files. However, the files were saved locally, which was not especially useful for a web app. Additionally, multer has some persisting issues that make it difficult to use the middleware when validating form data using express-validator. Although I ideally would upload an image at the same time as I create the post, there would be some problems around validating both the image upload and the blog post form data. So, I decided to do a little workaround by having the image be uploaded separately from the blog post form data.
As far as image storage goes, I chose to use AWS S3 because of its popularity and ability to meet my basic needs for the project. I was excited to try out S3 for the first time. As expected, I ran into a few errors. The first was related to needing to migrate to AWS SDK for JavaScript version 3, as I was using techniques for version 2. Once I migrated over, I was able to get the images to be stored in my S3 bucket. However, I struggled to figure out how to get the URL for the images so that I could save them in my database. Eventually, I figured out how to use the image name to create a URL based on an identified pattern that S3 uses for URLs.
The following image shows the S3 bucket for the blog. The URL for each image uploaded into the bucket follows the pattern: https://[BUCKET_NAME].s3.[AWS_REGION].amazonaws.com/[IMAGE_NAME]. To prevent duplicate image names, I used a universally unique identifier (UUID) as the prefix to the original name.
Building the client-facing site was relatively simple. I created two main pages:
A home page that displays the list of posts.
A page for the individual articles with accompanying reader comments.
React router was used for client-side routing between the pages. Until this point, I had solely been using IDs in URLs, making the URLs rather unattractive (e.g., blog.kristenmazza.dev/posts/64f8d66d2598b74c33e9fb4c). For this project, I decided to implement URL slugs to make the URLs a little more user-friendly. I took a brief break from working on the blog client and returned to the API to slugify the post titles (i.e., removing or modifying any characters not suitable for a URL), add the slug to the Post model when the document is saved, and use the slug identifier in the paths for retrieving the posts and comments. Upon returning to work on the blog client, all that was left was to get the slug and use it as part of the URLs when opening the articles.
Because the blog would be for my website, I knew that I would be the only one creating and editing posts, so I focused on getting the CMS functional without worrying about the appearance.
There were several features that I wanted to include:
A list of all posts, with the option to filter by published or draft status
A button to publish or unpublish posts
A WYSIWYG editor for creating new posts and updating existing posts
Image upload for the main blog post image
Comment management
I started by displaying the posts and allowing them to be filtered by whether they were published or drafts. I included a simple button to access the comment management page, where comments can be easily deleted for individual posts.
Then, I moved on to creating the blog post editor. I was eager to implement a WYSIWYG editor as part of the blog post creation and updating pages.
There are many options for WYSIWYG editors, but one editor I saw recommended frequently was TipTap. Looking through the documentation, I thought the editor appeared relatively simple in use and appearance (with options to customize), so I decided to try it out.
Implementing TipTap went relatively smoothly with one exception. I could create posts without a problem using TipTap, but I struggled with getting the TipTap editor to populate with existing blog content from the database when editing the posts. I found it interesting that the blog content would not show up in the editor initially, but it would miraculously appear after saving my code editor a second time. This told me that the editor content was being set before the blog content data was finished being fetched from the server. To fix this, I conditionally rendered a loading message until the content was available.
My next hurdle was figuring out how to upload images on the frontend. At first, my research into how to do this sent me down the wrong path, which involved presigning URLs to upload images in S3. However, I was eventually steered toward just focusing on how to upload to the backend endpoint that I had already established previously. Using axios and FormData, uploading images ended up being a lot simpler than it seemed during my first attempt.
While building this blog, my grasp of full-stack development deepened considerably, and I gained a clearer understanding of how the different pieces fit together. I initially considered implementing or started implementing various features that did not make it to the current iteration, such as enabling multiple authors to create posts. However, I chose not to pursue these features either because they were impractical for my personal blog's purposes or to maintain focus on making progress without investing excessive time in unnecessary functionalities. There are additionally some aspects I still plan to look into, such as improving the performance of the blog. I am looking forward to continuing to share my journey as I build my skillset with future projects and activities.
If you would like to contact me for any opportunities or just a chat, feel free to reach out via LinkedIn(opens in new tab).
© 2023 Kristen Mazza