Building a Full-Stack Blog Application: A Step-by-Step Tutorial

Santiago
45 min readMar 4, 2023

--

Blogging has become an essential tool for individuals and organizations alike to share their thoughts, opinions, and knowledge with the world. With the advancement of technology, creating and managing a blog has become easier than ever. In this post, we will discuss how to build a blog post app using React, Node.js and MySQL. React is a popular front-end JavaScript library for building user interfaces, while Node.js is a powerful back-end JavaScript runtime environment that can be used with MySQL, a popular open-source relational database management system to create scalable, robust and efficient web applications.

By combining these technologies, we can create a powerful and dynamic blog post app that will allow users to create, view, and interact with blog posts. We’ll be creating a database to store posts and user information using MySQL, we’ll use Node.js and Express to create a server that interacts with the database. Next, we’ll build a front-end user interface using React, allowing users to create and edit blog posts. By the end of this tutorial, you will have gained a solid understanding of how to integrate these technologies to create a fully functional web application. So let’s get started and learn how to build a blog post app with React, Node.js, and MySQL.

How will the application be?

On the Home page of our blog post application, we will employ a query to retrieve all posts stored in the MySQL database. Upon clicking a specific post, the application will execute a query to retrieve the details of the post, including the user who wrote it. This functionality will require a demonstration of how to join different tables within the MySQL database.

User registration and login features will be implemented and only authorized users, who are the creators of posts, will be granted editing and deleting permissions. Additionally, the application will offer to the users the ability to write posts using a rich text editor, allowing the application to manipulate text style, and upload images to use as post covers.

The application will also provide a sidebar displaying only similar posts to the one being viewed. This project will help developers in understanding the basic concepts of React and MySQL, database relationships, user authentication, JSON, web tokens for security, cookies manipulation, and other essential functionalities.

Quick overview of React and Node.js

React is a popular open-source JavaScript library used for building user interfaces. It was developed by Facebook and released in 2013. React uses a virtual DOM (Document Object Model) which is a lightweight copy of the actual DOM that allows React to efficiently update only the parts of the UI that need to be changed. This approach provides better performance compared to traditional approaches where the entire DOM is updated.

React’s component-based architecture is another key feature. Components are reusable pieces of code that define the structure and behavior of a part of the user interface. Components can be nested within each other, allowing developers to create complex UIs by combining smaller and simpler components. This approach promotes code reusability, maintainability, and testability.

Node.js is a JavaScript runtime built on Chrome’s V8 JavaScript engine. It allows developers to write server-side JavaScript code. This enables the use of a single language (JavaScript) for both client-side and server-side development. Node.js provides non-blocking I/O operations, which means that I/O operations do not block the event loop and can be executed asynchronously. This makes Node.js a great choice for building scalable, high-performance web applications.

Node.js also has a large and active community with a wide range of modules and packages available through the npm registry. These modules and packages provide functionality that can be easily integrated into Node.js applications, which speeds up development and improves the overall quality of the code.

Overall, React and Node.js are powerful tools that can be used to build modern web applications. Their popularity is driven by their ease of use, performance, and active communities.

let’s get started

We will initiate our project by creating a root directory and subsequently adding two subdirectories named ‘client’ (where React app will be located) and ‘api’ (will be the back-end of our app) within it.

To avoid creating unnecessary files when setting up a React application and to save time on cleaning up the project, please refer to the react-mini branch in my GitHub repository, where you can find a basic project structure to use as a starting point.

With that said, go to the ‘client’ folder and run the following command:

git clone --single-branch -b "react-mini" https://github.com/santiagobedoa/tutorial-blog_post_app.git .

Once cloned, run npm install to install all dependencies.

Note that the ‘pages’ folder found inside ‘src’ contains some .jsx files that will be the pages that our application will have, but how we’re going to reach all those pages? The answer is react-router-dom, but what is it?

react-roueter-dom

React Router DOM is a library that provides routing capabilities to a React application. It is built on top of the React Router library and provides a higher-level API for building client-side applications with routing. With React Router DOM, you can easily manage the application state and change the content displayed on the screen based on the URL path. It provides a set of components and methods that allow you to define routes and navigate between them using links or programmatic navigation. It also supports server-side rendering and integration with popular state management libraries such as Redux.

Example

Here is a simple example of how you can use react-router-dom to create a navigation bar with links to different pages in your React application:

import { BrowserRouter as Router, Route, Link } from "react-router-dom";

function Home() {
return <h2>Welcome to the Home page</h2>;
}

function About() {
return <h2>Learn more about us on the About page</h2>;
}

function Contact() {
return <h2>Contact us through the Contact page</h2>;
}

function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/about">About</Link>
</li>
<li>
<Link to="/contact">Contact</Link>
</li>
</ul>
</nav>

<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</div>
</Router>
);
}

export default App;

In this example, we’re using BrowserRouter as the router and Link to create links to different pages. We're also defining three different page components (Home, About and Contact) that are rendered when the user navigates to the corresponding URL path.

Note

The Router component is the root component of the routing system. It is responsible for keeping the current URL in sync with the current set of Route components that are being rendered.

The Route component is used to declare which components should be rendered for a specific URL path. It can take a path prop to specify the URL path and a component prop to specify the component to render for that path.

It’s important to have all Route components inside the Router component so that the router can match the current URL with the path props of the Route components and render the corresponding components. If the Route components were not inside the Router component, the router would not be able to match the current URL with the path props of the Route components and no components would be rendered.

react-router-dom implementation

Now you know the basics of react-router-dom let’s install it by running npm install react-router-dom(remember to be located in ‘client’ folder).

Before continuing we’re going to create the navbar and the footer that will be common components in our pages excluding the login and register page. Let’s create a folder within ‘src’ named components. Within the folder create two files Footer.jsx and Navbar.jsx. Add the following code to each file:

// Footer.jsx
import React from "react";

const Footer = () => {
return <div>Footer</div>;
};

export default Footer;
// Navbar.jsx
import React from "react";

const Navbar = () => {
return <div>Navbar</div>;
};

export default Navbar;

Now let’s add the routes to App.js file:

// App.js
// Import the necessary components from the react-router-dom package and other custom components
import { createBrowserRouter, RouterProvider, Outlet } from "react-router-dom";
import Home from "./pages/Home";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Single from "./pages/Single";
import Write from "./pages/Write";
import Navbar from "./components/Navbar";
import Footer from "./components/Footer";

// Create a Layout component that defines the structure of the web page
const Layout = () => {
return (
<>
<Navbar />
<Outlet />
<Footer />
</>
);
};

// Define the application routes and components using the createBrowserRouter function
const router = createBrowserRouter([
{
path: "/",
element: <Layout />,
// Remember that Home, Single post and Write are the pages that will have a Navbar and a Footer
children: [
{
path: "/",
element: <Home />,
},
{
path: "/post/:id",
element: <Single />,
},
{
path: "/write",
element: <Write />,
},
],
},
{
path: "/login",
element: <Login />,
},
{
path: "/register",
element: <Register />,
},
]);

// Define the App function that returns the RouterProvider component that provides the routing context to the entire app
function App() {
return (
<div className="app">
<div className="container">
<RouterProvider router={router} />
</div>
</div>
);
}

// Export the App component as the default export
export default App;

The code defines a Layout component that provides the basic structure of the web page, with a navigation bar, content, and a footer. It then defines the application routes using the createBrowserRouter function from react-router-dom, which takes an array of objects with path and element properties. The path property specifies the URL path, while the element property specifies the component to be rendered when the path is matched. The Layout component is used as a container for the routes, with the Outlet component used to render the nested routes inside the Layout.

Finally, the App function provides the routing context to the entire app using the RouterProvider component, which takes the router object as a prop. The router object contains all the defined routes. The App function returns a div containing the RouterProvider component wrapped in a container div with the class container.

Now, if you run your app using npm start and visit http://localhost:3000/ you should see something like:

Navbar
Home
Footer

Or if you visit http://localhost:3000/write you should see:

Navbar
Write
Footer

With this, I hope you have a clear understanding of how routes and the Outlet component work.

Basic Front-end

As the primary objective of this tutorial is to illustrate the integration of React, Node.js, and MySQL to develop a functional application, I will omit the fundamental front-end creation phase, which is more aligned with HTML and CSS writing practices than React programming. However, a template for the basic front-end will be available in the basic-frontend branch of the project repository, which can be used for further development.

Make sure

  • You have modified the following files located in client/src/pages/: Home.jsx, Login.jsx, Register.jsx, Single.jsx, Write.jsx
  • You have modified the following files located client/src/components/: Footer.jsx, Menu.jsx,Navbar.jsx
  • You have created client/src/style.scss file.
  • npm install sass: will be used to style our front-end. But, what is sass? short for Syntactically Awesome Stylesheets, is a preprocessor scripting language that is compiled into CSS. It extends the functionality of CSS by providing a variety of features such as variables, nested rules, mixins, functions, and more. Sass allows developers to write more maintainable and scalable stylesheets with less repetition and boilerplate code, and it can be integrated into a wide range of development workflows, including front-end frameworks like React, Vue, and Angular. Sass files can be compiled into regular CSS files that can be used in web applications.
  • npm install react-quill: will be used to write articles. But, what is react-quill? is a React-based rich text editor component that allows users to format and edit text using a toolbar with a variety of options. It is built on top of the Quill.js library, and provides a flexible and customizable interface for creating rich text editors in React applications. With React Quill, developers can easily add rich text editing capabilities to their applications, and allow users to create and format text content with ease.

If you start the application ( npm start ) it should look like this:

basic-frontend Login.jsx
basic-frontend Register.jsx
basic-frontend Home.jsx
basic-frontend Single.jsx

Before you go any further, I recommend that you stop and review each of the components that were created and make sure you understand the code, as we will be working on this code from now on.

Back-end

We will proceed to navigate to the API folder, and initialize a Node application through the npm init -y command. Prior to continuing, let's install the necessary libraries that we will be working with. First, we will install express via npm install express which will provide us with the ability to create our server. We will also be utilizing mysql2 by installing it via npm install mysql2, and finally, nodemon through npm install nodemon to enable auto-refresh upon changes.

We will modify the script “test” to “start” in the package.json file and set it to "nodemon index.js" command in order to refresh the server automatically. Additionally, we will add "types": "modules" in the same file to allow importing libraries in the ES6 module format. package.json should looks like:

{
"name": "api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.2",
"mysql2": "^3.1.2",
"nodemon": "^2.0.20"
}
}

Now let’s create index.js file, where we will create our server:

// index.js
import express from "express";

// Create an instance of the Express application
const app = express();

// Use the built-in JSON middleware to parse incoming requests
app.use(express.json());

// Start the server and listen on port 8800
app.listen(8800, () => {
console.log("Connected...");
});

This code sets up an Express application with a JSON middleware and starts a server on port 8800. The app constant is an instance of the Express application, which is used to define the routes and middleware for the server. The express.json() middleware is used to parse incoming JSON data in request bodies. Finally, the app.listen() method is called to start the server and listen on port 8800.

After executing the npm start command in your terminal, you should see the message “Connected…” which indicates that the server is listening to incoming requests on the port 8800 and ready to handle them appropriately.

MySQL Connection

To be able to perform CRUD operations in our Node.js application, we need to establish a connection with the MySQL database. In order to do that, we will use the mysql2 library. We will create a new file called db.js inside the API folder to hold the database connection details.

// db.js
import mysql from "mysql2";

export const db = mysql.createConnection({
host: "localhost",
user: "root",
password: null,
database: "blog_app",
});

In order to establish the connection to the MySQL server, the mysql2 library is used with a createConnection() method that receives an object with the configuration of the connection. It is important to note that depending on the configuration of your MySQL server, the host, user, and password fields may vary. In this example, for practicality, the root user is being used, which doesn't have a password. However, it's recommended to create a new user with a password and store this information in an .env file. This file can be accessed using the dotenv library, which loads the environment variables stored in the .env file into process.env so that they can be accessed throughout the application. This is a common practice in production to avoid exposing sensitive information like database credentials in the source code.

You are probably wondering why the name of the database is “blog_app” if we have never created it at any time. So let’s create it:

-- setup_mysql.sql
-- create database
CREATE DATABASE IF NOT EXISTS blog_app;
-- create table users
CREATE TABLE IF NOT EXISTS `blog_app`.`users` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`password` VARCHAR(255) NOT NULL,
`img` VARCHAR(255) NULL,
PRIMARY KEY (`id`)
);
-- create table posts
CREATE TABLE IF NOT EXISTS `blog_app`.`posts` (
`id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`desc` VARCHAR(1000) NOT NULL,
`img` VARCHAR(255) NOT NULL,
`cat` VARCHAR(255) NOT NULL,
`date` DATETIME NOT NULL,
`uid` INT NOT NULL,
PRIMARY KEY (`id`),
INDEX `uid_idx` (`uid` ASC) VISIBLE,
CONSTRAINT `uid`
FOREIGN KEY (`uid`)
REFERENCES `blog_app`.`users` (`id`)
ON DELETE CASCADE
ON UPDATE CASCADE
);

The above code is a set of SQL commands that creates a new database named “blog_app” and two tables: “users” and “posts”.

The “users” table has columns named “id”, “username”, “email”, “password”, and “img”. The “id” column is set as the primary key for the table.

The “posts” table has columns named “id”, “title”, “desc”, “img”, “cat”, “date”, and “uid”. The “id” column is set as the primary key and the “uid” column has a foreign key constraint that references the “id” column in the “users” table. The “INDEX” command creates an index on the “uid” column for faster querying. The “ON DELETE CASCADE” and “ON UPDATE CASCADE” commands ensure that any changes to the “users” table are reflected in the “posts” table.

To create the database and tables, you can execute the setup_mysql.sql file in the MySQL server. If you're using root user without password, you can run cat setup_mysql.sql | mysql -u root in the terminal. In case you have set a password for the root user, you can add the -p flag at the end of the command, which will prompt you to enter the password.

Route Controller Structure

To modularize our code and improve maintainability, we’ll move away from placing all routes and functions in index.js. Instead, we’ll create a new directory called “routes” within the API directory. Inside this directory, we’ll have files for each endpoint of our application, including auth.js, posts.js, and users.js. Each of these files will define the routes for the endpoint and the corresponding function to be executed when a request is made to that route. This approach helps us create specific functions that perform well-defined tasks, making our code more modular and easier to manage.

Example:

// api/routes/posts.js
import express from "express";

// Creating a new router instance
const router = express.Router();

// Defining a new route on this router for the GET HTTP method for the '/test' endpoint
// When the '/test' endpoint is reached, the provided callback function will execute
router.get("/test", (req, res) => {
res.json("this is post");
});

// Exporting this router module so that it can be used elsewhere in the application
export default router;

Let’s import router to index.js

// api/index.js
import express from "express";
// Import router as postRoutes
import postRoutes from "./routes/posts.js";

// Create an instance of the Express application
const app = express();

// Use the built-in JSON middleware to parse incoming requests
app.use(express.json());

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

// Start the server and listen on port 8800
app.listen(8800, () => {
console.log("Connected...");
});

Now if you run the server ( npm start ) and visit http://localhost:8800/api/posts/test you should get “this is post”. But as we said before, is important to modularize our code. So we are going to create another folder inside api named controller that will contain the following files: auth.js, posts.js, and users.js. Within these files we will have all the functions in charge of doing the CRUD operations.

Example:

Let’s add the following code to controller/posts.js:

// controller/posts.js
// Exporting a function called testPost that takes two parameters, req and res, which represent the request and response objects, respectively.
export const testPost = (req, res) => {
// Sending a JSON response with a string message "this is post from controller" when this function is called.
res.json("this is post from controller");
};

Modify routes/posts.js:

// routes/posts.js
import express from "express";
import { testPost } from "../controller/posts.js";

// Creating a new router instance
const router = express.Router();

// Defining a new route on this router for the GET HTTP method for the '/test' endpoint
// When the '/test' endpoint is reached, testPost function will execute
router.get("/test", testPost);

// Exporting this router module so that it can be used elsewhere in the application
export default router;

Now if you visit http://localhost:8800/api/posts/test you should get “this is post from controller” instead “this is post”.

Up to this point, the API folder structure should look like this:

api
├── controller
│ ├── auth.js
│ ├── posts.js
│ └── users.js
├── routes
│ ├── auth.js
│ ├── posts.js
│ └── users.js
├── db.js
├── index.js
├── package.json
└── setup_mysql.sql

Authentication with JWT and Cookie

Now that we have the basic structure of our project. Let’s create our authentication system. For this we will need to install:

  • jsonbwebtoken: in a nutshell, jsonwebtoken is a compact, URL-safe means of representing claims to be transferred between two parties. These claims can be used to authenticate and authorize users in web applications. The library provides methods for generating, signing, and verifying JSON Web Tokens (JWTs) using a secret key or public/private key pair.
  • bcryptjs: is a JavaScript library used for password hashing. It uses the bcrypt algorithm to securely hash passwords and protect them from attacks such as brute-force and rainbow table attacks. Bcryptjs generates a salt for each password and then hashes the salted password multiple times, making it more difficult for an attacker to crack the password even if they have access to the hashed values. It is commonly used in web development to store user passwords in a secure manner.

Important! In cryptography, a salt is a random value that is used as an additional input to a one-way function, such as a cryptographic hash function. The purpose of the salt is to make it more difficult to perform attacks such as precomputed hash attacks and dictionary attacks, which try to guess the input of the hash function. By adding a salt to the input, the resulting hash will be different, even if the input is the same. This makes it much more difficult for an attacker to crack the hashed password. The salt value is typically stored alongside the hashed password in a database.

To install this libraries run npm install bcryptjs jsonwebtoken.

Register BACK-END

The first thing we need to do is create a function that allows us to register new users , let’s add the following function to controller/auth.js file:

// controller/auth.js
import { db } from "../db.js";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";

// This function is responsible for registering a new user in the database
export const register = (req, res) => {
// CHECK EXISTING USER
// SQL query to check if the user already exists in the database
const query = "SELECT * FROM users WHERE email = ? OR username = ?";
// Execute the query with the user's email and username as parameters
db.query(query, [req.body.email, req.body.username], (err, data) => {
// Check for errors
if (err) return res.json(err);
// If the query returns data, it means the user already exists, return a 409 conflict status code
if (data.length) return res.status(409).json("User already exists!");

// Hash the password and create a user
// Generate a salt value
const salt = bcrypt.genSaltSync(10);
// Generate a hash value using the password and the salt value
const hash = bcrypt.hashSync(req.body.password, salt);

// SQL query to insert the new user in the database
const query = "INSERT INTO users(`username`,`email`,`password`) VALUES (?)";
// Define the values to be inserted in the query, including the hashed password
const values = [req.body.username, req.body.email, hash];

// Execute the query with the values as parameters
db.query(query, [values], (err, data) => {
// Check for errors
if (err) return res.json(err);
// If successful, return a 200 status code with a message
return res.status(200).json("User has been created.");
});
});
};

This code exports a function named “register” that handles a POST request for registering a new user. The function first checks if the user already exists in the database by executing a SELECT query with the user’s email and username. If the query returns any data, it means the user already exists, so the function returns a response with a 409 status code and a message “User already exists!”.

If the user does not exist, the function hashes the user’s password using bcrypt library and creates a new user by executing an INSERT query with the user’s username, email, and hashed password. If the query is successful, the function returns a response with a 200 status code and a message “User has been created.”

The function uses the db object to execute the SQL queries, which should be established in a separate file (db.js) to establish a connection with the database.

Let’s add the required routes to perform the authentication, let’s open routes/auth.js file and add the following code:

// routes/auth.js
// importing express and the necessary controller functions
import express from "express";
import { login, logout, register } from "../controller/auth.js";

// creating a new router instance
const router = express.Router();

// defining routes for register, login, and logout
router.post("/register", register);
router.post("/login", login);
router.post("/logout", logout);

// exporting the router instance
export default router;

Note that login and logout functions haven’t been implemented yet.

Finally, let’s implement authentication routes to our application. index.js file should looks like:

// index.js
// Import the Express library
import express from "express";
// Import routes
import authRoutes from "./routes/auth.js";
import postRoutes from "./routes/posts.js";

// Create an instance of the Express application
const app = express();

// Use the built-in JSON middleware to parse incoming requests
app.use(express.json());

// Routes
app.use("/api/auth", authRoutes);
app.use("/api/posts", postRoutes);

// Start the server and listen on port 8800
app.listen(8800, () => {
console.log("Connected...");
});

The benefits of modularized code become apparent when considering the specific responsibilities of each function. While it’s possible to centralize database connections, application routes, and data manipulation functions in index.js, separating each function improves maintainability by providing clear points of reference in the event of an issue. It's optimal to create an index.js file to handle the creation of our Express application with all necessary middleware, a db.js file to establish the database connection, a routes/ directory to contain all application routes, and a controller/ directory to house data processing and CRUD operations.

Register FRONT-END

In order to test the registration of a user, we must make some changes in the front-end. First we need to install axios (npm install axios) to be able to communicate with the back-end.

Axios is a popular JavaScript library used to make HTTP requests from a web browser or Node.js. It provides a simple and easy-to-use API for making requests to RESTful web services, and supports a wide range of features such as sending form data, setting headers, handling request and response interceptors, and canceling requests. Axios can be used in both client-side and server-side applications, and is widely used in modern web development for communicating with APIs and fetching data from servers.

Let’s make the changes to registration page (Register.jsx):

// Register.jsx
import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import axios from "axios";

const Register = () => {
const [inputs, setInputs] = useState({
// setting initial state for inputs using useState hook
username: "",
email: "",
password: "",
});

const [err, setError] = useState(null); // setting initial state for error using useState hook

const navigate = useNavigate(); // using useNavigate hook from react-router-dom to navigate

const handleChange = (e) => {
// function to handle input changes
setInputs((prev) => ({ ...prev, [e.target.name]: e.target.value }));
// using spread operator to spread previous state and update the current input
};

const handleSubmit = async (e) => {
// function to handle form submit
e.preventDefault(); // preventing the default form submission behavior
try {
await axios.post("/auth/register", inputs); // making a post request to register the user using axios library
navigate("/login"); // navigating to login page after successful registration
} catch (err) {
// handling errors if any
setError(err.response.data); // setting error message from response data
}
};

return (
<div className="auth">
{/* containing the authentication form */}
<h1>Register</h1>
<form>
<input
required
type="text"
placeholder="username"
name="username"
onChange={handleChange} // triggering handleChange function on input change
/>
<input
required
type="email"
placeholder="email"
name="email"
onChange={handleChange} // triggering handleChange function on input change
/>
<input
required
type="password"
placeholder="password"
name="password"
onChange={handleChange} // triggering handleChange function on input change
/>
<button onClick={handleSubmit}>Register</button>{" "}
{/* triggering handleSubmit function on form submit */}
{err && <p>{err}</p>} {/* displaying error message if there's any */}
<span>
Do you have an account? <Link to="/login">Login</Link>{" "}
{/* providing link to login page */}
</span>
</form>
</div>
);
};

export default Register; // exporting Register component

When a user enters data into the input fields, the handleChange function updates the state accordingly.

When the user clicks the “Register” button, the handleSubmit function is triggered. This function makes an HTTP POST request to the server with the user’s registration information, and if successful, navigates the user to the login page using the useNavigate hook. If there’s an error during registration, the error message is displayed on the page.

Finally, we need to add "proxy":http//localhost:8800/api at the end of client/package.json file. Why? It allows the server to proxy requests to the API server during development.

This is useful when the React project is hosted on a different server than the API server, as it allows the frontend code to make requests to the backend API without worrying about cross-origin resource sharing (CORS) issues.

By specifying the proxy in package.json, requests made to paths beginning with “/api” will be automatically forwarded to the specified server (in this case, http://localhost:8800).

Now, if you fill the registration form, you must be redirected to the login page and the user must be added to the database (“users” table).

Login and Logout

let’s apply the same logic for the login and logout functions. Add this two functions to the bottom of the controller/auth.js file:

// This function handles user login
export const login = (req, res) => {
// SQL query to check if the user exists in the DB
const query = "SELECT * FROM users WHERE username = ?";

// Execute the query with the provided username
db.query(query, [req.body.username], (err, data) => {
// Handle DB errors
if (err) return res.json(err);

// If no user is found, return an error
if (data.length === 0) return res.status(404).json("User not found!");

// Check if the password is correct
const isPasswordCorrect = bcrypt.compareSync(
req.body.password,
data[0].password
);

// If the password is incorrect, return an error
if (!isPasswordCorrect)
return res.status(400).json("Wrong username or password!");

// If the login is successful, create a JSON web token
const token = jwt.sign({ id: data[0].id }, "jwtkey");

// Remove the password from the user data
const { password, ...other } = data[0];

// Set the token as a http-only cookie and send user data as response
res
.cookie("access_token", token, {
httpOnly: true,
})
.status(200)
.json(other);
});
};

// This function handles user logout
export const logout = (req, res) => {
// Clear the access_token cookie and send a success message
res
.clearCookie("access_token", {
sameSite: "none",
secure: true,
})
.status(200)
.json("User has been logged out.");
};

The login function:

The first step is to construct a SQL query to retrieve the user data from the database using the provided username, stored in req.body.username. The query variable stores the SQL statement.

The db.query() method executes the query, with the username as a parameter. The callback function provides two arguments: err and data. If there is an error, the function returns a JSON response with the error details.

If the query returns no data, which means that there is no user with the provided username, the function returns a 404 status with an error message as a JSON response.

If the user is found, the function compares the provided password, req.body.password, with the hashed password stored in the database, using the bcrypt.compareSync() method. If the comparison fails, the function returns a 400 status with an error message as a JSON response.

If the login credentials are correct, the function generates a JSON web token with the user’s id as payload, using the jwt.sign() method.

Then, the password is removed from the data object using destructuring assignment, and the user data, excluding the password, is stored in the other variable.

Finally, the generated token is set as an HTTP-only cookie, and the user data is sent as a JSON response with a 200 status code.

Note: jwt.sign() is a method provided by the JSON Web Token (JWT) package that is used to create a new JWT. A JWT is a compact and self-contained token that contains information (i.e., the payload) that can be used to verify the identity of the user. The jwt.sign() method takes two parameters: the payload and a secret key. The payload is the data that will be stored in the JWT, and it can be any JavaScript object that can be serialized as JSON. The secret key is a string that is used to sign the token, ensuring that the token has not been tampered with.

When the jwt.sign() method is called, it creates a new JWT that includes the payload and a signature that is generated using the secret key. This JWT is then returned as a string, which can be sent to the client and stored in a cookie or local storage. When the client sends a request to the server, the server can verify the identity of the user by decoding the JWT and checking the payload. If the signature is valid and the payload is trusted, the server can use the information in the payload to authenticate the user and respond accordingly.

The logout function:

The function first calls the clearCookie() method on the res (response) object, which removes the access_token cookie from the user's browser. It takes two arguments: the name of the cookie to clear ("access_token"), and an object containing additional options for the cookie. In this case, the options sameSite: "none" and secure: true are set, which ensure that the cookie is only sent over HTTPS and is not limited to same-site requests.

cookie-parser

As you may have noticed, we are working with cookies. They are important in web development for a number of reasons:

  1. Session management: Cookies are often used to manage user sessions on websites. When a user logs in, the website sets a cookie containing a unique session ID, which is then sent with each subsequent request from the user. This allows the server to associate subsequent requests with the same session, and to keep track of user-specific data, such as user preferences or shopping cart items.
  2. Personalization: Cookies can be used to personalize a user’s experience on a website. For example, a website may use cookies to remember a user’s language preference or to show personalized recommendations based on the user’s browsing history.
  3. Tracking: Cookies can be used to track user behavior on a website, such as which pages they visit or which products they view. This data can be used to improve the website’s user experience, as well as for advertising and marketing purposes.

Overall, cookies are a powerful tool for web developers to create more personalized and dynamic web experiences for users, while also providing important functionality such as session management. They are not as bad as they are painted. As developers we have to be careful with the third point, because this is when the line of ethics becomes blurred.

In order to work with cookies in our back-end we need to install cookie-parser (npm install cookie-parser) and specify to the server to use cookie-parser as middleware:

//index.js
import express from "express";
import authRoutes from "./routes/auth.js";
import postRoutes from "./routes/posts.js";
import cookieParser from "cookie-parser";

// Create an instance of the Express application
const app = express();

// Use the built-in JSON middleware to parse incoming requests
app.use(express.json());
// Use the cookieParser middleware to parse cookies from incoming requests
app.use(cookieParser());

// Routes
app.use("/api/auth", authRoutes);
app.use("/api/posts", postRoutes);

// Start the server and listen on port 8800
app.listen(8800, () => {
console.log("Connected...");
});

Login FRONT-END

Finally, we need to make some changes to the login page:

import React, { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import axios from "axios";

// Define a functional component called Login
const Login = () => {
// Use useState hook to create state variables for inputs and errors
const [inputs, setInputs] = useState({
username: "",
password: "",
});

const [err, setError] = useState(null);

// Use useNavigate hook to create a navigate function
const navigate = useNavigate();

// Define handleChange function to update the input state variables when the user types into the input fields
const handleChange = (e) => {
setInputs((prev) => ({ ...prev, [e.target.name]: e.target.value }));
};

// Define handleSubmit function to handle the form submission when the user clicks the submit button
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Post the user input to the "/auth/login" endpoint and navigate to the home page
await axios.post("/auth/login", inputs);
navigate("/");
} catch (err) {
// If there is an error, set the error state variable to the error message
setError(err.response.data);
}
};

// Render the login form with input fields for username and password and a button to submit the form
return (
<div className="auth">
<h1>Login</h1>
<form>
<input
type="text"
placeholder="username"
name="username"
onChange={handleChange}
/>
<input
type="password"
placeholder="password"
name="password"
onChange={handleChange}
/>
<button onClick={handleSubmit}>Login</button>
{err && <p>{err}</p>}
<span>
Don't you have an account? <Link to="/register">Register</Link>
</span>
</form>
</div>
);
};

export default Login;

It is not worth explaining what was done since the exact same logic was applied as in the Register page. Now, if you login with the user previously registered, you must be redirected to the home page.

React Context API

It is recommended to persist user information in local storage for efficient retrieval across different React components to present data and enable specific actions. As such, user data should be stored in local storage to ensure availability and accessibility across components.

For this purpose, we’re going to use react-context. React context is a feature of React that allows data to be passed down the component tree without the need to pass props manually at every level. It provides a way to share data between components, even if they are not directly related in the component hierarchy. Context is often used for global state management, such as user authentication and theme settings. It consists of two parts: a Provider component that provides the data to its children, and a Consumer component that consumes the data. The useContext hook can also be used to consume context data in functional components.

To begin, we will create a directory named ‘context’, within our client source code, where we will store the file ‘authContext.js’. The full path to this file should be client/src/context/authContext.js. This file will serve as the context for our application:

// src/context/authContext.js
import axios from "axios";
import { createContext, useEffect, useState } from "react";

// Create a new context called AuthContext
export const AuthContext = createContext();

// Define a component called AuthContextProvider that takes in children as props
export const AuthContextProvider = ({ children }) => {
// Initialize a state variable called currentUser and set it to either the user object in local storage or null if it does not exist
const [currentUser, setCurrentUser] = useState(
JSON.parse(localStorage.getItem("user")) || null
);

// Define a function called login that makes a POST request to the /auth/login endpoint with the given inputs and sets the currentUser state variable to the response data
const login = async (inputs) => {
const res = await axios.post("/auth/login", inputs);
setCurrentUser(res.data);
};

// Define a function called logout that makes a POST request to the /auth/logout endpoint and sets the currentUser state variable to null
const logout = async (inputs) => {
await axios.post("/auth/logout");
setCurrentUser(null);
};

// Store the currentUser state variable in local storage whenever it changes
useEffect(() => {
localStorage.setItem("user", JSON.stringify(currentUser));
}, [currentUser]);

// Return the AuthContext.Provider component with the currentUser, login, and logout functions as values and the children as its child components
return (
<AuthContext.Provider value={{ currentUser, login, logout }}>
{children}
</AuthContext.Provider>
);
};

This code defines a context for managing authentication in a React application. It exports an AuthContext object created with the createContext function, which can be used to provide authentication information to child components in the component tree.

The AuthContextProvider component is also exported, which is a wrapper around the child components that provides the authentication state and methods for logging in and logging out. It uses the useState hook to maintain the current user's information in the component's state. The user's information is initially set by retrieving the stored user object from the browser's local storage using localStorage.getItem("user").

The login function is defined to perform a login operation by sending a POST request to the /auth/login endpoint with the user's input data. If the request is successful, the current user's state is updated with the response data.

The logout function sends a POST request to the /auth/logout endpoint to log out the user. It then updates the current user's state to null.

The useEffect hook is used to store the current user's information to the browser's local storage whenever the currentUser state changes.

Finally, the AuthContext.Provider component is returned, which provides the currentUser, login, and logout values as the context value to its child components. This enables other components in the application to access the authentication state and methods.

Since we have functions in charge of doing the login and logout in the app using context, we must make some changes in Login.jsx:

  //useContext hook to get the login function from the AuthContext.  
const { login } = useContext(AuthContext);

// Define handleSubmit function to handle the form submission when the user clicks the submit button
const handleSubmit = async (e) => {
e.preventDefault();
try {
// Post the user input to the "/auth/login" endpoint and navigate to the home page
await login(inputs); // new login function
navigate("/");
} catch (err) {
// If there is an error, set the error state variable to the error message
setError(err.response.data);
}
};

Last, we need to ensure that our application is wrapped with the Provider component so that the components can access the state data and actions provided by the context. Specifically, the App component will be the child component of the Provider component.

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { AuthContextProvider } from "./context/authContext";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<AuthContextProvider>
<App />
</AuthContextProvider>
</React.StrictMode>
);

Let’s apply the same logic to the Navbar:

// Navbar.jsx
import React, { useContext } from "react";
import { Link, useNavigate } from "react-router-dom";
import { AuthContext } from "../context/authContext";
import Logo from "../images/logo.png";

const Navbar = () => {
const { currentUser, logout } = useContext(AuthContext);
const navigate = useNavigate();

const logoutNavbar = () => {
logout();
navigate("/login");
};

return (
<div className="navbar">
<div className="container">
<div className="logo">
<a href="/">
<img src={Logo} alt="logo" />
</a>
</div>
<div className="links">
<Link className="link" to="/?cat=art">
<h6>ART</h6>
</Link>
<Link className="link" to="/?cat=science">
<h6>SCIENCE</h6>
</Link>
<Link className="link" to="/?cat=technology">
<h6>TECHNOLOGY</h6>
</Link>
<Link className="link" to="/?cat=cinema">
<h6>CINEMA</h6>
</Link>
<Link className="link" to="/?cat=design">
<h6>DESIGN</h6>
</Link>
<Link className="link" to="/?cat=food">
<h6>FOOD</h6>
</Link>
<span>{currentUser?.username}</span>
{currentUser ? (
<span onClick={logoutNavbar}>Logout</span>
) : (
<Link className="link" to="/login">
Login
</Link>
)}
<span className="write">
<Link className="link" to="/write">
Write
</Link>
</span>
</div>
</div>
</div>
);
};

export default Navbar;

Now if you login, you should see the username in the navbar, and if you logout you must be redirected to the login page and localstorage should be empty.

Fetch MySQL data

Now we will retrieve information about the posts. However, before proceeding, I would like to provide you with an SQL script to insert data into the database. I recommend to delete any pre-existing data to avoid conflicts. Note that the password in the script is already encrypted. To login use the password “test” which was used for the two users.

-- data_mysql.sql
-- add users
INSERT INTO `blog_app`.`users` (`id`, `username`, `email`, `password`, `img`) VALUES ('1', 'Santiago', 'santiago@mail.com', '$2a$10$VqZJYEYnauC8qrVrVpWZX.E0u95i40pBmAEOc.Vs178nUJNyenzaa', 'https://images.unsplash.com/photo-1633332755192-727a05c4013d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80');
INSERT INTO `blog_app`.`users` (`id`, `username`, `email`, `password`, `img`) VALUES ('2', 'Pablo', 'pablo@mail.com', '$2a$10$VqZJYEYnauC8qrVrVpWZX.E0u95i40pBmAEOc.Vs178nUJNyenzaa', 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1760&q=80');

-- insert posts
insert into `blog_app`.`posts` (`id`, `title`, `desc`, `img`, `uid`, `cat`, `date`) VALUES ('1', 'Lorem ipsum dolor sit amet consectetur adipisicing elit', 'Lorem, ipsum dolor sit amet consectetur adipisicing elit. A possimus excepturi aliquid nihil cumque ipsam facere aperiam at! Ea dolorem ratione sit debitis deserunt repellendus numquam ab vel perspiciatis corporis!', 'https://images.unsplash.com/photo-1513364776144-60967b0f800f?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2671&q=80', '1', 'art', '2023-02-08');
insert into `blog_app`.`posts` (`id`, `title`, `desc`, `img`, `uid`, `cat`, `date`) VALUES ('2', 'Lorem ipsum dolor sit amet consectetur adipisicing elit', 'Lorem, ipsum dolor sit amet consectetur adipisicing elit. A possimus excepturi aliquid nihil cumque ipsam facere aperiam at! Ea dolorem ratione sit debitis deserunt repellendus numquam ab vel perspiciatis corporis!', 'https://images.unsplash.com/photo-1536924940846-227afb31e2a5?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1766&q=80', '2', 'food', '2023-02-08');
insert into `blog_app`.`posts` (`id`, `title`, `desc`, `img`, `uid`, `cat`, `date`) VALUES ('3', 'Lorem ipsum dolor sit amet consectetur adipisicing elit', 'Lorem, ipsum dolor sit amet consectetur adipisicing elit. A possimus excepturi aliquid nihil cumque ipsam facere aperiam at! Ea dolorem ratione sit debitis deserunt repellendus numquam ab vel perspiciatis corporis!', 'https://images.unsplash.com/photo-1546069901-ba9599a7e63c?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1180&q=80', '2', 'art', '2023-02-08');
insert into `blog_app`.`posts` (`id`, `title`, `desc`, `img`, `uid`, `cat`, `date`) VALUES ('4', 'Lorem ipsum dolor sit amet consectetur adipisicing elit', 'Lorem, ipsum dolor sit amet consectetur adipisicing elit. A possimus excepturi aliquid nihil cumque ipsam facere aperiam at! Ea dolorem ratione sit debitis deserunt repellendus numquam ab vel perspiciatis corporis!', 'https://images.unsplash.com/photo-1493770348161-369560ae357d?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1770&q=80', '1', 'food', '2023-02-08');

Now we have two users and some posts created in the database we can proceed. First let’s add the routes that we are going to use:

// api/routes/posts.js
import express from "express";
import {
addPost,
deletePost,
getPost,
getPosts,
updatePost,
} from "../controller/posts.js";

// Create a new Router object
const router = express.Router();

// Define routes for various HTTP methods and their corresponding functions
router.get("/", getPosts); // Get all posts
router.get("/:id", getPost); // Get a specific post by its ID
router.post("/", addPost); // Add a new post
router.delete("/:id", deletePost); // Delete a post by its ID
router.put("/:id", updatePost); // Update a post by its ID

export default router;

Now let’s create the logic in controller/posts.js:

import { db } from "../db.js";
import jwt from "jsonwebtoken";

// Retrieves posts from a database
export const getPosts = (req, res) => {
// If the query string includes a category parameter,
// select all posts from the given category. Otherwise,
// select all posts.
const q = req.query.cat
? "SELECT * FROM posts WHERE cat=?"
: "SELECT * FROM posts";

// Use the database object to query the database with the
// appropriate SQL statement and any necessary parameters.
db.query(q, [req.query.cat], (err, data) => {
// If there's an error, send a 500 status code and the error message
if (err) return res.status(500).send(err);

// Otherwise, send a 200 status code and the data as JSON
return res.status(200).json(data);
});
};

// Retrieves a single post from the database
export const getPost = (req, res) => {
// Select specific fields from both the users and posts table,
// and join them based on the user ID of the post author.
const q =
"SELECT p.id, `username`, `title`, `desc`, p.img, u.img AS userImg, `cat`,`date` FROM users u JOIN posts p ON u.id = p.uid WHERE p.id = ?";

// Use the database object to query the database for the post with
// the given ID, and any necessary parameters.
db.query(q, [req.params.id], (err, data) => {
// If there's an error, send a 500 status code and the error message
if (err) return res.status(500).json(err);

// Otherwise, send a 200 status code and the first item in the data array as JSON
return res.status(200).json(data[0]);
});
};

// Adds a new post to the database
export const addPost = (req, res) => {
// Check if the user is authenticated by checking for a token in the cookies
const token = req.cookies.access_token;
if (!token) return res.status(401).json("Not authenticated!");

// Verify the token using the secret key
jwt.verify(token, "jwtkey", (err, userInfo) => {
// If there's an error, the token is not valid
if (err) return res.status(403).json("Token is not valid!");

// Otherwise, construct the SQL query to insert a new post into the database
const q =
"INSERT INTO posts(`title`, `desc`, `img`, `cat`, `date`,`uid`) VALUES (?)";

// Define an array of values to be inserted into the database, including the
// post data from the request body and the user ID from the decoded token
const values = [
req.body.title,
req.body.desc,
req.body.img,
req.body.cat,
req.body.date,
userInfo.id,
];

// Use the database object to execute the SQL query with the values array
db.query(q, [values], (err, data) => {
// If there's an error, return a 500 status code and the error message
if (err) return res.status(500).json(err);

// Otherwise, return a 200 status code and a success message
return res.json("Post has been created.");
});
});
};

// Deletes a post from the database
export const deletePost = (req, res) => {
// Check if the user is authenticated by checking for a token in the cookies
const token = req.cookies.access_token;
if (!token) return res.status(401).json("Not authenticated");

// Verify the token using the secret key
jwt.verify(token, "jwtkey", (err, userInfo) => {
// If there's an error, the token is not valid
if (err) return res.status(403).json("Token is not valid");

// Otherwise, get the ID of the post to be deleted from the request parameters
const postId = req.params.id;

// Construct an SQL query to delete the post with the specified ID, but only if
// the user ID associated with the post matches the ID of the authenticated user
const q = "DELETE FROM posts WHERE `id` = ? AND `uid` = ?";

// Execute the SQL query with the postId and userInfo.id as parameters
db.query(q, [postId, userInfo.id], (err, data) => {
// If there's an error, return a 403 status code and an error message
if (err) return res.status(403).json("You can delete only your post");

// Otherwise, return a 200 status code and a success message
return res.json("Post has been deleted");
});
});
};

// Update a post
export const updatePost = (req, res) => {
// Get the access token from the request cookies.
const token = req.cookies.access_token;

// Check if the token exists, if not, return an error response.
if (!token) return res.status(401).json("Not authenticated!");

// Verify the token using the "jwtkey" secret key. If the token is not valid, return an error response.
jwt.verify(token, "jwtkey", (err, userInfo) => {
if (err) return res.status(403).json("Token is not valid!");

// Get the post ID from the request parameters.
const postId = req.params.id;

// SQL query to update the post with new values.
const q =
"UPDATE posts SET `title`=?,`desc`=?,`img`=?,`cat`=? WHERE `id` = ? AND `uid` = ?";

// An array containing the new values for the post.
const values = [req.body.title, req.body.desc, req.body.img, req.body.cat];

// Execute the query using the values and post ID. If there's an error, return an error response. Otherwise, return a success response.
db.query(q, [...values, postId, userInfo.id], (err, data) => {
if (err) return res.status(500).json(err);
return res.json("Post has been updated.");
});
});
};

This code defines several functions to interact with a database.

The getPosts function retrieves all posts from the database, or only those from a specific category if a category parameter is provided in the request.

The getPost function retrieves a single post from the database based on its ID, along with information about the user who created it.

The addPost function adds a new post to the database, but only if the user is authenticated with a valid JSON Web Token (JWT).

The deletePost function deletes a post from the database, but only if the authenticated user is the one who created the post.

The updatePost function updates an existing post in the database, but only if the authenticated user is the one who created the post. Like addPost and deletePost, this function also requires a valid JWT for authentication.

Home page

Given that we have established endpoints within our backend to fetch post data, the next step is to incorporate these endpoints into the Home page component in order to retrieve data from the database and display it accordingly.

// Home.jsx
import axios from "axios";
import React, { useEffect, useState } from "react";
import { Link, useLocation } from "react-router-dom";

const Home = () => {
// Declaring a state variable called posts and initializing it to an empty array
const [posts, setPosts] = useState([]);

// Getting the current URL query string (if any) using the useLocation hook from react-router-dom
const cat = useLocation().search;

// Defining an effect that runs when the cat variable changes
useEffect(() => {
// Defining an asynchronous function called fetchData
const fetchData = async () => {
try {
// Making an HTTP GET request to the server to retrieve posts data based on the cat variable
const res = await axios.get(`/posts${cat}`);
// Updating the posts state variable with the retrieved data
setPosts(res.data);
} catch (err) {
// Logging any errors that occur during the request
console.log(err);
}
};
// Calling the fetchData function
fetchData();
}, [cat]); // Specifying that this effect should only run when the cat variable changes

// Defining a helper function called getText that takes an HTML string and returns the text content
const getText = (html) => {
const doc = new DOMParser().parseFromString(html, "text/html");
return doc.body.textContent;
};

// Rendering the Home component
return (
<div className="home">
<div className="posts">
{/* Mapping over the posts state variable and rendering a Post component for each post */}
{posts.map((post) => (
<div className="post" key={post.id}>
<div className="post-img">
{/* Rendering the post image */}
<img src={`${post.img}`} alt="post cover" />
</div>
<div className="content">
{/* Rendering a link to the post page */}
<Link className="link" to={`/post/${post.id}`}>
<h1>{post.title}</h1>
</Link>
{/* Rendering the post description */}
<p>{getText(post.desc)}</p>
{/* Rendering a button to read more */}
<Link className="link" to={`/post/${post.id}`}>
<button>Read More</button>
</Link>
</div>
</div>
))}
</div>
</div>
);
};

export default Home;

This is a React component that displays a list of blog posts. The component retrieves data from a server based on the current URL query string and uses state to store and render the retrieved data. It also defines a helper function to extract text content from HTML strings. The component maps over the retrieved data and renders a Post component for each post, which includes the post image, title, description, and a link to read more.

If you visit the “art” section you should see only the posts that contain the “art” category, the same should happen for the “food” category:

Art:

Food:

Single page: Fetch user and single post

// Single.jsx
import React, { useContext, useEffect, useState } from "react";
import EditImage from "../images/edit.png";
import DeleteImage from "../images/delete.png";
import { Link, useLocation, useNavigate } from "react-router-dom";
import Menu from "../components/Menu";
import axios from "axios";
import moment from "moment";
import { AuthContext } from "../context/authContext";

const Single = () => {
const [post, setPost] = useState({});

// The useLocation hook returns the current location object, which contains information about the current URL.
const location = useLocation();
// The useNavigate hook returns a navigate function that can be used to navigate to a new location.
const navigate = useNavigate();

// Extract the post ID from the current URL.
const postId = location.pathname.split("/")[2];

// Get the current user from the AuthContext.
const { currentUser } = useContext(AuthContext);

// Use the useEffect hook to fetch the blog post data from the server when the component mounts.
useEffect(() => {
const fetchData = async () => {
try {
const res = await axios.get(`/posts/${postId}`);
setPost(res.data);
} catch (err) {
console.log(err);
}
};
fetchData();
}, [postId]);

// Handler function for deleting a blog post.
const handleDelete = async () => {
try {
await axios.delete(`/posts/${postId}`);
// Navigate to the home page after deleting the post.
navigate("/");
} catch (err) {
console.log(err);
}
};

// Helper function to extract plain text from an HTML string.
const getText = (html) => {
const doc = new DOMParser().parseFromString(html, "text/html");
return doc.body.textContent;
};

// Render the blog post.
return (
<div className="single">
<div className="content">
{/* Render the post image. */}
<img src={`${post?.img}`} alt="post cover" />
<div className="user">
{/* Render the user image if it exists. */}
{post.userImg && <img src={post.userImg} alt="user" />}
<div className="info">
{/* Render the post author and date. */}
<span>{post.username}</span>
<p>Posted {moment(post.date).fromNow()}</p>
</div>
{/* Render the edit and delete buttons if the current user is the author of the post. */}
{currentUser.username === post.username && (
<div className="edit">
<Link to={`/write?edit=2`} state={post}>
<img src={EditImage} alt="edit" />
</Link>
<img onClick={handleDelete} src={DeleteImage} alt="delete" />
</div>
)}
</div>
{/* Render the post title and description. */}
<h1>{post.title}</h1>
<i>"{getText(post.desc)}"</i>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
Sollicitudin nibh sit amet commodo nulla facilisi nullam vehicula
ipsum. Enim lobortis scelerisque fermentum dui faucibus in ornare
quam. Viverra justo nec ultrices dui sapien. Aliquam nulla facilisi
cras fermentum odio eu feugiat pretium. Suscipit adipiscing bibendum
est ultricies integer quis auctor elit. Eu volutpat odio facilisis
mauris. Consectetur adipiscing elit pellentesque habitant morbi
tristique. Tristique senectus et netus et malesuada fames. Convallis a
cras semper auctor neque. Sed felis eget velit aliquet sagittis id
consectetur purus ut. Eu feugiat pretium nibh ipsum consequat nisl vel
pretium lectus. Ac odio tempor orci dapibus. Velit scelerisque in
dictum non consectetur a. Nibh tellus molestie nunc non blandit massa
enim nec. Rutrum quisque non tellus orci ac auctor augue mauris.
</p>
<p>
Diam sollicitudin tempor id eu nisl nunc mi ipsum faucibus. Eu
facilisis sed odio morbi quis commodo. Scelerisque mauris pellentesque
pulvinar pellentesque. Tortor aliquam nulla facilisi cras fermentum.
Accumsan lacus vel facilisis volutpat est. Nam libero justo laoreet
sit. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Cum
sociis natoque penatibus et magnis. Nunc scelerisque viverra mauris in
aliquam sem. Id porta nibh venenatis cras sed. Ac tortor vitae purus
faucibus ornare suspendisse. Montes nascetur ridiculus mus mauris
vitae ultricies leo integer malesuada. Turpis tincidunt id aliquet
risus. Sed adipiscing diam donec adipiscing tristique risus nec.
Tempor id eu nisl nunc mi ipsum faucibus. Iaculis urna id volutpat
lacus laoreet non. Neque volutpat ac tincidunt vitae semper quis
lectus. Vitae ultricies leo integer malesuada nunc vel risus commodo.
Cras sed felis eget velit aliquet sagittis id consectetur. Eros donec
ac odio tempor orci dapibus ultrices in iaculis.
</p>
<p>
Ut eu sem integer vitae. Aliquam vestibulum morbi blandit cursus risus
at. Convallis aenean et tortor at risus viverra adipiscing at. Sit
amet dictum sit amet justo donec enim. Nulla aliquet porttitor lacus
luctus accumsan tortor. Ultrices mi tempus imperdiet nulla malesuada
pellentesque elit eget gravida. Dui sapien eget mi proin sed. Urna
nunc id cursus metus aliquam eleifend mi in. Euismod quis viverra nibh
cras pulvinar mattis. Consequat nisl vel pretium lectus quam id leo in
vitae. Turpis egestas integer eget aliquet nibh praesent tristique
magna. Dui accumsan sit amet nulla facilisi. Risus ultricies tristique
nulla aliquet enim tortor at auctor.
</p>
<p>
Morbi tincidunt ornare massa eget egestas purus viverra accumsan in.
Eget nunc lobortis mattis aliquam. Quisque non tellus orci ac auctor.
Gravida quis blandit turpis cursus in hac habitasse platea dictumst.
Id neque aliquam vestibulum morbi blandit cursus risus. Orci porta non
pulvinar neque laoreet suspendisse interdum. In nibh mauris cursus
mattis molestie a. Phasellus faucibus scelerisque eleifend donec
pretium vulputate. Dis parturient montes nascetur ridiculus mus mauris
vitae ultricies. Elit scelerisque mauris pellentesque pulvinar. Enim
praesent elementum facilisis leo vel fringilla est ullamcorper eget.
</p>
</div>
<Menu cat={post.cat} />
</div>
);
};

export default Single;

The useState hook initializes a state variable called post to an empty object and provides a function called setPost to update the state. The useLocation hook returns the current location object, which contains information about the current URL. The useNavigate hook returns a navigate function that can be used to navigate to a new location. The useContext hook is used to get the current user from the AuthContext. The useEffect hook is used to fetch the blog post data from the server when the component mounts. The handleDelete function is used to delete a blog post. The getText function is a helper function to extract plain text from an HTML string. Finally, the component returns a JSX element that renders the blog post content, including the post image, author and date information, edit and delete buttons, and post title and description.

The moment library (npm install moment) was used to get the current date to insert into the DB. Note: the description found in the database is in quotation marks and italics. The paragraphs with Lorem Ipsum were left to fill out the information page (aesthetic purposes only).

Recommended posts

Let’s modify Menu.jsx:

// Menu.jsx
import axios from "axios";
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";

// Defining a functional component named Menu which takes a single prop named cat
const Menu = ({ cat }) => {
// Initializing posts state with an empty array using useState hook
const [posts, setPosts] = useState([]);

// useEffect hook is used to fetch posts related to the category
useEffect(() => {
// Defining an async function fetchData to fetch posts related to the category using axios
const fetchData = async () => {
try {
const res = await axios.get(`/posts/?cat=${cat}`);
setPosts(res.data);
} catch (err) {
console.log(err);
}
};
// Calling fetchData function to fetch data when component is mounted or when category is changed
fetchData();
}, [cat]);

return (
<div className="menu">
<h1>Other posts you may like</h1>
{posts.map((post) => (
<div className="post" key={post.id}>
<img src={`${post.img}`} alt="post cover" />
<h2>{post.title}</h2>
{/* Using Link component to navigate to the post */}
<Link className="link" to={`/post/${post.id}`}>
<button>Read More</button>
</Link>
</div>
))}
</div>
);
};

export default Menu;

As you can see, the logic applied to this component is quite similar to the one we have used for the others. We utilize the useEffect hook to invoke axios for data fetching and employ the useState hook (posts, setPosts) to retain the response. Subsequently, we execute the rendering of posts, pertaining to a specific category, from the “posts” variable.”

Write post

Before we dive into implementing the article writing feature, let’s first decide how we want to save the images that users attach to their posts. There are several options we could use, like cloud storage services, but for this tutorial, we’re going to keep it simple and show you how to upload your images directly to your server.

Let’s install multer (npm install multer) which is a popular middleware for handling file uploads in Node.js. Multer is used in conjunction with Express.js and allows us to easily handle multipart/form-data, which is typically used when uploading files through HTML forms. With Multer, we can easily specify where to save uploaded files, limit the size of files that can be uploaded, and perform various validations on the files being uploaded.

Let’s add multer to index.js in the api server:

// api/index.js

// Use the multer middleware to storage images
// Configuration object for setting destination and filename for the uploaded file
const storage = multer.diskStorage({
destination: function (req, file, cb) {
// Set the destination folder where the uploaded file should be stored
cb(null, "../client/public/upload");
},
filename: function (req, file, cb) {
// Set the filename of the uploaded file
cb(null, Date.now() + file.originalname);
},
});

// Set up multer middleware with the defined storage configuration
const upload = multer({ storage });

// Set up a POST endpoint for handling file uploads
app.post("/api/upload", upload.single("file"), function (req, res) {
// Get the uploaded file
const file = req.file;
// Send a response with the filename of the uploaded file
res.status(200).json(file.filename);
});

Don’t forget to import multer!

Now that we have where to store the image, let’s fetch them. Add the following code to the Write component:

// Write.jsx
import React, { useState } from "react";
import ReactQuill from "react-quill";
import "react-quill/dist/quill.snow.css";
import axios from "axios";
import { useLocation, useNavigate } from "react-router-dom";
import moment from "moment";

const Write = () => {
// Get the location state using the `useLocation` hook
// will be used to check if we are in writing o edit mode
const state = useLocation().state;

// Define the state variables
const [value, setValue] = useState(state?.title || "");
const [title, setTitle] = useState(state?.desc || "");
const [file, setFile] = useState(null);
const [cat, setCat] = useState(state?.cat || "");

// Define the navigate function
const navigate = useNavigate();

// Define the upload function
const upload = async () => {
try {
// Create a new FormData object and append the file to it
const formData = new FormData();
formData.append("file", file);

// Send a POST request to upload the file
const res = await axios.post("/upload", formData);

// Return the filename of the uploaded file
return res.data;
} catch (err) {
console.log(err);
}
};

// Define the handleClick function to handle the form submission
const handleClick = async (e) => {
e.preventDefault();

// Upload the image and get the filename
const imgUrl = await upload();

try {
// Send a PUT request to update a post if the location state is defined (writing),
// otherwise send a POST request to create a new post
state
? await axios.put(`/posts/${state.id}`, {
title,
desc: value,
cat,
img: file ? imgUrl : "",
})
: await axios.post(`/posts/`, {
title,
desc: value,
cat,
img: file ? imgUrl : "",
date: moment(Date.now()).format("YYYY-MM-DD HH:mm:ss"),
});

// Navigate to the homepage after the post is saved or updated
navigate("/");
} catch (err) {
console.log(err);
}
};

return (
<div className="add">
<div className="content">
<input
type="text"
placeholder="Title"
onChange={(e) => setTitle(e.target.value)}
/>
<div className="editor-container">
<ReactQuill
className="editor"
theme="snow"
value={value}
onChange={setValue}
/>
</div>
</div>
<div className="menu">
<div className="item">
<h1>Publish</h1>
<span>
<b>Status: </b> Draft
</span>
<span>
<b>Visibility: </b> Public
</span>
<input
style={{ display: "none" }}
type="file"
id="file"
name=""
onChange={(e) => setFile(e.target.files[0])}
/>
<label className="file" htmlFor="file">
Upload Image
</label>
<div className="buttons">
<button>Save as a draft</button>
<button onClick={handleClick}>Publish</button>
</div>
</div>
<div className="item">
<h1>Category</h1>
<div className="cat">
<input
type="radio"
checked={cat === "art"}
name="cat"
value="art"
id="art"
onChange={(e) => setCat(e.target.value)}
/>
<label htmlFor="art">Art</label>
</div>
<div className="cat">
<input
type="radio"
checked={cat === "science"}
name="cat"
value="science"
id="science"
onChange={(e) => setCat(e.target.value)}
/>
<label htmlFor="science">Science</label>
</div>
<div className="cat">
<input
type="radio"
checked={cat === "technology"}
name="cat"
value="technology"
id="technology"
onChange={(e) => setCat(e.target.value)}
/>
<label htmlFor="technology">Technology</label>
</div>
<div className="cat">
<input
type="radio"
checked={cat === "cinema"}
name="cat"
value="cinema"
id="cinema"
onChange={(e) => setCat(e.target.value)}
/>
<label htmlFor="cinema">Cinema</label>
</div>
<div className="cat">
<input
type="radio"
checked={cat === "design"}
name="cat"
value="design"
id="design"
onChange={(e) => setCat(e.target.value)}
/>
<label htmlFor="design">Design</label>
</div>
<div className="cat">
<input
type="radio"
checked={cat === "food"}
name="cat"
value="food"
id="food"
onChange={(e) => setCat(e.target.value)}
/>
<label htmlFor="food">Food</label>
</div>
</div>
</div>
</div>
);
};

export default Write;

It is crucial to utilize the useLocation hook in order to determine the current state of the application. This is especially important when editing an existing post, as the useLoction().state will provide us with the post ID as well as other pertinent information such as the post’s title and description. By utilizing this information, we can distinguish whether we are creating a new post or modifying an existing one.

Finally, we should change ${post.img} to ../upload/${post.img} in your Home, Single and Menu components to properly render the images that because we are using multer.

What’s next?

This tutorial provides a comprehensive guide on integrating React, Node.js, and MySQL to develop a full-stack application. It covers the deployment of a server with crucial middleware, routes, and controllers using the Express framework. You have learned how to establish a connection from the server to the database and perform CRUD (Create, Read, Update, Delete) operations from the client to the server.

Great job on completing the tutorial! Now, if you want to take your app to the next level, here’s a fun challenge for you: add a feature that allows users to upload a profile image when they register. You could also create a user panel where users can edit their profile information such as their name, email, password, and profile picture. Don’t worry if you’re not sure where to start, the route and user controller are ready for you to get creative with! Have fun!

Important

This project was created by “Lama Dev,” a passionate developer who shares valuable insights and tutorials on their YouTube channel. You can watch the full tutorial series that inspired this blog post here.

It’s essential to understand that this blog and the associated code were developed with a primary focus on learning and educational purposes. I believe that one of the best ways to truly grasp and internalize knowledge is by explaining it to others. In that spirit, this blog serves as a comprehensive guide to integrating React, Node.js, and MySQL to build a full-stack application.

Find me on

Linkedin

Github

--

--