A full-stack blog application built with Express.js, MySQL, and React. Users can sign up, write and publish blog posts, view a public feed, search for other authors, and manage their own profiles.
- Features
- Tech Stack
- Project Structure
- Database Design
- Prerequisites
- Getting Started
- Environment & Configuration
- API Reference
- Deployment
- Known Issues & Notes
- User registration and login
- Write and publish blog posts
- Public feed showing all posts with author names
- Author profile pages with age calculation from date of birth
- Edit first name and date of birth on your own profile
- Delete your own account
- Search for users by first name
- Protected routes β write, profile, and search pages require login
- Session persisted in localStorage
- MySQL (local via XAMPP / phpMyAdmin or cloud via Railway)
Blog/
βββ Backend/
β βββ src/
β βββ DB/
β β βββ connection.db.js # MySQL connection
β βββ modules/
β β βββ auth/
β β β βββ auth.controller.js
β β β βββ auth.service.js # signup, login
β β βββ blog/
β β β βββ blog.controller.js
β β β βββ blog.service.js # createBlog, listBlogs
β β βββ user/
β β βββ user.controller.js
β β βββ user.service.js # getProfile, search, update, delete
β βββ app.controller.js # Express app bootstrap
β βββ index.js # Entry point
β
βββ Frontend/
βββ Blog-Forge/
βββ src/
βββ context/
β βββ AuthContext.jsx
β βββ BlogContext.jsx
β βββ UserContext.jsx
βββ pages/
β βββ Home.jsx
β βββ Login.jsx
β βββ Signup.jsx
β βββ WriteBlog.jsx
β βββ Profile.jsx
β βββ SearchUsers.jsx
βββ components/
β βββ Navbar.jsx
β βββ BlogCard.jsx
β βββ ProtectedRoute.jsx
βββ utils/
β βββ api.js # Axios instance
βββ App.jsx
βββ main.jsx
βββ index.css
USER
| Attribute | Notes |
|---|---|
| id | Primary Key |
| firstName | Part of composite name attribute |
| middleName | Part of composite name attribute |
| lastName | Part of composite name attribute |
| DOB | Date of Birth |
| age | Derived attribute (calculated from DOB) |
| gender | |
| confirmEmail | |
| phone | Multivalued attribute |
| createdAt | |
| updatedAt |
Blog
| Attribute | Notes |
|---|---|
| id | Primary Key |
| title | |
| content | |
| createdAt | |
| updatedAt |
Relationship: A USER can create one or many Blog posts (1-to-many relationship).
USERS
| Column | Constraint |
|---|---|
| id | Primary Key (PK) |
| firstName | |
| middleName | |
| lastName | |
| DOB | |
| gender | |
| confirmEmail | |
| createdAt | |
| updatedAt |
ageis a derived attribute and is not stored as a column β it is computed fromDOBat query level.
USERS_Phone
Separate table created because phone is a multivalued attribute.
| Column | Constraint |
|---|---|
| phone | |
| userId | Foreign Key referencing USERS(id) |
Blog
| Column | Constraint |
|---|---|
| id | Primary Key (PK) |
| title | |
| content | |
| createdAt | |
| updatedAt | |
| authorId | Foreign Key referencing USERS(id) |
- The
phoneattribute is multivalued, so it is extracted into its own tableUSERS_Phonelinked back toUSERSvia a foreign key. - The
nameattribute is composite, so it is broken down intofirstName,middleName, andlastNameas individual columns. - The
ageattribute is derived and is calculated at the query level fromDOBrather than stored. - The 1-to-many relationship between
USERSandBlogis represented by theauthorIdforeign key in theBlogtable.
ERD
Relational Mapping
- Start your MySQL server (e.g. via XAMPP).
- Open phpMyAdmin at
http://localhost/phpmyadmin. - Create a database named
Blog App. - Create the
userstable:
CREATE TABLE users (
u_id INT AUTO_INCREMENT PRIMARY KEY,
u_firstName VARCHAR(100),
u_middleName VARCHAR(100),
u_lastName VARCHAR(100),
u_email VARCHAR(255) UNIQUE NOT NULL,
u_password VARCHAR(255) NOT NULL,
u_DOB DATE,
u_confirmEmail TINYINT DEFAULT 0,
u_createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
u_updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
gender VARCHAR(10)
);- Create the
users_phonetable:
CREATE TABLE users_phone (
phone VARCHAR(20),
userId INT,
FOREIGN KEY (userId) REFERENCES users(u_id) ON DELETE CASCADE
);- Create the
blogstable:
CREATE TABLE blogs (
b_id INT AUTO_INCREMENT PRIMARY KEY,
b_title VARCHAR(255) NOT NULL,
b_content TEXT NOT NULL,
b_author_id INT,
b_createdAt DATETIME DEFAULT CURRENT_TIMESTAMP,
b_updatedAt DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (b_author_id) REFERENCES users(u_id) ON DELETE CASCADE
);cd Blog/Backend
npm installUpdate src/DB/connection.db.js with your MySQL credentials if needed:
export const connection = mySql2.createConnection({
database: "Blog App",
port: "3306",
password: "", // your MySQL root password
user: "root",
});Start the backend:
node src/index.jsThe server runs on http://localhost:3000.
cd Blog/Frontend/Blog-Forge
npm install
npm run devThe frontend runs on http://localhost:5173.
Make sure the backend is running before using the app.
The frontend communicates with the backend via src/utils/api.js:
const api = axios.create({
baseURL: "http://localhost:3000",
});Change baseURL to your deployed backend URL when deploying to production.
| Method | Endpoint | Body | Description |
|---|---|---|---|
| POST | /auth/signup | firstName, middleName, lastName, email, password, confirmPassword | Register a user |
| POST | /auth/login | email, password | Login a user |
| Method | Endpoint | Body | Description |
|---|---|---|---|
| POST | /blog | title, content, authorId | Create a blog post |
| GET | /blog | β | List all blog posts |
| Method | Endpoint | Body / Params | Description |
|---|---|---|---|
| GET | /user/:id/profile | id (param) | Get user profile |
| GET | /user/search | searchKey (query) | Search users by name |
| PATCH | /user/:id | firstName, DOB | Update user profile |
| DELETE | /user/:id | id (param) | Delete a user |
| Layer | Service |
|---|---|
| Frontend | Vercel |
| Backend | Railway |
| Database | Railway MySQL |
- Export your local database from phpMyAdmin and import it into a Railway MySQL instance.
- Update
connection.db.jswith the Railway MySQL credentials. - Add a
vercel.jsonto the backend root:
{
"version": 2,
"builds": [{ "src": "src/index.js", "use": "@vercel/node" }],
"routes": [{ "src": "/(.*)", "dest": "src/index.js" }]
}- Deploy the backend to Vercel or Railway.
- Update
src/utils/api.jsin the frontend with the deployed backend URL. - Deploy the frontend to Vercel.
- Passwords are stored in plain text. In production you should hash passwords using bcrypt before storing them.
- There is no JWT authentication. After login the raw user object is stored in localStorage. A proper implementation should use signed tokens.
- The
listBlogsendpoint returns HTTP status201instead of200β this is a minor backend bug that does not affect functionality. - CORS is enabled globally with
cors(). In production you should restrict it to your frontend domain only: