Welcome to the MERN Stack! This guide will help you set up a MERN stack application step-by-step.
- Node.js and npm: Ensure that Node.js and npm (Node Package Manager) are installed on your machine. These are essential for running JavaScript on the server and managing dependencies.
- Basic knowledge of JavaScript: Familiarity with JavaScript syntax and concepts will help you follow along more easily.
We'll start by organizing our project into two main folders:
frontend
: This folder will contain all the files related to the client-side of our application, which is built using React.js.backend
: This folder will contain the server-side code, where we'll handle data processing, business logic, and communication with the database.
The backend of a MERN application is where the core logic resides. It handles tasks such as processing requests, interacting with databases, and sending responses. We'll use Express.js, a fast and minimalist web framework for Node.js, to build our server.
Start by navigating to the backend
folder where we'll set up our server:
cd backend
We need to initialize our backend project by creating a package.json
file. This file keeps track of our project's metadata and dependencies.
npm init -y
Express.js is a popular framework for building web servers in Node.js. It simplifies the process of handling HTTP requests, routing, and middleware.
npm install express
Now, create a file named index.js
. This file will contain the code to set up a basic Express server. The server listens for incoming requests and responds with a simple message.
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Hello World");
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
To start the server, we'll use Nodemon, a tool that automatically restarts the server whenever you make changes to your code. This makes the development process more efficient.
nodemon index.js
If you haven't installed nodemon
, you can do so globally with:
npm install -g nodemon
Open your browser and navigate to http://localhost:3000
to see the "Hello World" message from your server.
Routing is a critical aspect of any web application. It determines how an application responds to different HTTP requests (e.g., GET, POST) for specific endpoints (URLs). Express.js provides a powerful and flexible routing system that helps you manage the different parts of your application more effectively.
Routes help you define specific endpoints in your application and map them to the corresponding logic. This makes your code more modular and easier to maintain.
To keep our routes organized, we'll create a new folder named router
inside the backend
folder. Within this folder, create a file named auth-router.js
. This file will handle the routes related to authentication.
Add the following code to auth-router.js
:
const express = require("express");
const router = express.Router();
// Basic route setup using Router
router.route("/").get((req, res) => {
res.send("Hello World using Router");
});
module.exports = router;
Now, we'll modify index.js
to include our new router. This allows us to separate concerns and keep our main server file clean.
const express = require("express");
const app = express();
const router = require("./router/auth-router");
app.use("/api/auth", router);
app.get("/", (req, res) => {
res.send("Hello World");
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
With these changes:
- Visiting
http://localhost:3000
will still display "Hello World". - Visiting
http://localhost:3000/api/auth
will display "Hello World using Router", showcasing our new route setup.
Controllers are a key part of the MVC (Model-View-Controller) design pattern. They are responsible for processing incoming requests, interacting with the data layer (models), and sending responses back to the client. By using controllers, we can keep our application logic organized and modular.
In the backend
folder, create a new folder named controllers
and a file named auth-controller.js
. This file will contain the logic for handling authentication-related requests.
Add the following code to auth-controller.js
:
const home = async (req, res) => {
try {
res.send("Welcome to the home page using controllers");
} catch (error) {
console.log(error);
}
};
const register = async (req, res) => {
try {
res.send("Welcome to the register page using controller");
} catch (error) {
console.log(error);
}
};
module.exports = { home, register };
Next, update the auth-router.js
file to use the new controller functions. This will link the routes to the appropriate logic in the controllers.
const express = require("express");
const router = express.Router();
const { home, register } = require("../controllers/auth-controller");
router.route("/").get(home);
router.route("/register").post(register);
module.exports = router;
- Visit
http://localhost:3000
to see the basic "Hello World" message. - Visit
http://localhost:3000/api/auth
to see the "Hello World using Router" message. - Visit
http://localhost:3000/api/auth/register
to see the "Welcome to the register page using controller" message.
Middlewares are functions that execute during the lifecycle of a request to the Express server. They have access to the request object, response object, and the next middleware function in the applicationβs request-response cycle. Middlewares are crucial for adding functionality such as parsing request bodies, handling authentication, or logging requests.
To test API requests, we'll need to parse incoming JSON data. Express provides a built-in middleware express.json()
that parses incoming requests with JSON payloads. It should be added at the beginning of your middleware stack to ensure it is available for all subsequent route handlers.
const express = require("express");
const app = express();
const router = require("./router/auth-router");
// Middleware
app.use(express.json());
app.use("/api/auth", router);
app.get("/", (req, res) => {
res.send("Hello World");
});
const PORT = 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
Ensure the register route is set to handle POST requests, as this is the standard for creating resources or submitting data.
const express = require("express");
const router = express.Router();
const { home, register } = require("../controllers/auth-controller");
router.route("/").get(home);
router.route("/register").post(register);
module.exports = router;
Modify the register
function in auth-controller.js
to handle and respond with the JSON data:
const home = async (req, res) => {
try {
res.send("Welcome to the home page using controller");
} catch (error) {
console.log(error);
}
};
const register = async (req, res) => {
try {
console.log(req.body); // Log the incoming request body
res.json({ message: "User registered successfully", data: req.body });
} catch (error) {
console.log(error);
}
};
module.exports = { home, register };
To test the API with Postman:
- Set the request type to
POST
. - Enter
http://localhost:3000/api/auth/register
in the URL. - In the Headers tab, add
Content-Type
as the key andapplication/json
as the value. - In the Body tab, select
raw
andJSON
from the dropdown, then enter the following JSON:
{
"email": "amankrsinha07@gmail.com",
"password": 12334
}
- Click
Send
to make the request. You should see the response containing the JSON data you sent.
In this section, we'll connect our backend to MongoDB, a NoSQL database that stores data in flexible, JSON-like documents. We will use Mongoose, an ODM (Object Data Modeling) library, to interact with MongoDB in a more structured and efficient way.
First, install the necessary packages for MongoDB connection and environment variable management:
npm install mongodb mongoose dotenv
To use MongoDB in a cloud environment, follow these steps to set up MongoDB Atlas:
-
Create a MongoDB Atlas Account:
- Visit MongoDB Atlas and log in or sign up for an account.
-
Create a New Project:
-
Configure Network Access:
-
Create a Database User:
-
Create a Cluster:
-
Connect to Your Cluster:
-
After the cluster is created, click on the Connect button.
-
Choose a connection method
-
Get the connection string, which will look something like this:
mongodb+srv://<username>:<password>@<cluster_name>.mongodb.net
-
Create a .env
file in the root directory of the backend
folder and add the following content:
PORT=3000
MONGO_URI=mongodb+srv://<username>:<password>@<cluster_name>.mongodb.net
Replace <username>
, <password>
, and <cluster_name>
with the actual values from your MongoDB Atlas connection string.
To manage the MongoDB connection, create a new folder named utils
in the backend
directory and inside it, create a file named db.js
with the following code:
const mongoose = require("mongoose");
const URI = process.env.MONGO_URI;
const connectDB = async () => {
if (!URI) {
console.error("MongoDB URI is not defined");
process.exit(1); // Exit if URI is not set
}
try {
const conn = await mongoose.connect(URI, {
dbName: "mern", // Replace with your actual database name
});
console.log("MongoDB Database Connected Successfully");
} catch (error) {
console.error(`Error: ${error.message}`);
process.exit(1);
}
};
module.exports = connectDB;
This utility file will handle the connection to the MongoDB database.
Now, modify your index.js
file to use the connectDB
function for connecting to MongoDB before starting the server:
require("dotenv").config();
const express = require("express");
const app = express();
const router = require("./router/auth-router");
const connectDB = require("./utils/db");
// Middleware
app.use(express.json());
app.use("/api/auth", router);
app.get("/", (req, res) => {
res.send("Hello World");
});
// Connect to the database and start the server
connectDB()
.then(() => {
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
console.log("Database connected");
})
.catch((err) => {
console.error("Database connection failed", err);
process.exit(1);
});
- Schema:
- Defines the structure of the documents within a MongoDB collection.
- Specifies the fields, their types, and additional constraints or validation rules.
- Model:
- A higher-level abstraction that interacts with the database based on the defined schema.
- Represents a collection and provides an interface for querying, creating, updating, and deleting documents in that collection.
- Models are created from schemas and enable you to work with MongoDB data in a more structured manner in your application.
-
Create the
model
Folder:- In the
backend
directory, create a new folder namedmodel
.
- In the
-
Create the
user-model.js
File:- Inside the
model
folder, create a file nameduser-model.js
. - Define the
userSchema
andUser
model as follows:
- Inside the
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
phone: {
type: Number,
required: true,
},
password: {
type: String,
required: true,
},
isAdmin: {
type: Boolean,
default: false,
},
});
const User = mongoose.model("User", userSchema);
module.exports = User;
- Fields:
username
,email
,phone
, andpassword
are required fields.email
is also unique, meaning no two users can have the same email address.isAdmin
is a boolean field with a default value offalse
, indicating whether the user has admin privileges.
In this step, we will update our auth-controller.js
to store registered user data in the MongoDB database. We'll validate the input data, check if the user already exists, and then create a new user record if everything is valid.
Make the following changes to your auth-controller.js
:
const User = require("../models/user-model");
const home = async (req, res) => {
try {
res.send("Welcome to the home page using controller");
} catch (error) {
console.error(error);
}
};
const register = async (req, res) => {
try {
console.log(req.body);
const { username, email, phone, password } = req.body;
// Check if any field is empty
if (!username || !email || !phone || !password) {
return res.status(400).json({ message: "Please fill all the fields" });
}
// Check if the user already exists (email)
const userExists = await User.findOne({ email });
if (userExists) {
return res.status(400).json({ message: "User already exists" });
}
// Create a new user
const user = await User.create({
username,
email,
phone,
password,
});
res.status(201).json({ message: "User registered successfully", user });
console.log(user);
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server error. Please try again later." });
}
};
module.exports = { home, register };
-
Field Validation: The code first checks if all required fields (
username
,email
,phone
, andpassword
) are provided. If any field is missing, it returns a400 Bad Request
response with a message indicating that all fields must be filled. -
User Existence Check: It then checks if a user with the same email already exists in the database. If the user is found, it returns a
400 Bad Request
response with a message indicating that the user already exists. -
User Creation: If the user does not exist, the code creates a new user document in the database with the provided data and returns a
201 Created
response, indicating that the user was successfully registered. -
Error Handling: Any errors during the process are caught, logged, and a
500 Internal Server Error
response is sent to the client.
Now, you can test the registration functionality using Postman:
-
Open Postman:
- Set the request type to
POST
. - Enter the URL:
http://localhost:3000/api/auth/register
.
- Set the request type to
-
Headers:
- Ensure the
Content-Type
header is set toapplication/json
.
- Ensure the
-
Body:
- Select
raw
and chooseJSON
from the dropdown. - Enter the following JSON data in the body:
{ "username": "aman", "email": "amankrsinha07@gmail.com", "phone": "9876543210", "password": "12345" }
- Select
-
Send the Request:
- Click
Send
to register the new user. - If successful, you should see a response with the message "User registered successfully" and the user data.
- Click
By following these steps, you've successfully implemented a feature to store registered user data in MongoDB. You've also learned how to validate input data, check for existing users, and handle errors, all while testing the functionality using Postman.
This is a crucial step in building your MERN stack application, as user registration is often the first interaction users have with your backend system.
To enhance the security of your application, it is crucial to hash passwords before storing them in the database. This ensures that even if your database is compromised, the stored passwords remain secure.
First, you'll need to install the bcryptjs
package, which will help you hash passwords.
npm i bcryptjs
Now, modify your auth-controller.js
to hash the user's password before saving it to the database:
const bcrypt = require("bcryptjs");
// Hashing the password before creating user
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(String(password), saltRounds);
// Create a new user with the hashed password
const user = await User.create({
username,
email,
phone,
password: hashedPassword,
});
-
Bcrypt.js Import: We first import
bcryptjs
to use its hashing functionality. -
Salt Rounds: The
saltRounds
variable defines the cost factor, which is the number of times the hashing algorithm will be applied. A higher number increases the computation time, making it harder for attackers to crack the password. -
Hashing the Password:
- The
bcrypt.hash()
method takes two arguments: the password to hash and the number of salt rounds. - The
String(password)
ensures that the password is converted to a string before hashing. - The hashed password is then stored in the
hashedPassword
variable.
- The
-
Creating the User:
- Instead of storing the plain password, we store the
hashedPassword
in the database when creating a new user.
- Instead of storing the plain password, we store the
After implementing the changes, you can test the registration functionality again using Postman, as you did in Day 6. This time, when you check the user data in MongoDB Atlas or Compass, you should see the password field containing a hashed value instead of the plain text password.
By hashing passwords before storing them, you've taken an essential step toward securing user data in your application. Bcrypt.js is a widely-used and trusted library for password hashing, making it an excellent choice for this task.
JSON Web Tokens (JWT) are an open standard (RFC 7519) that defines a compact and self-contained way of securely transmitting information between parties as a JSON object. JWTs are widely used for authentication and authorization in web applications.
- Authentication: JWTs help verify the identity of a user or a client.
- Authorization: JWTs determine what actions a user or client is allowed to perform.
- Header: Contains metadata about the token, such as the type of token and the signing algorithm being used.
- Payload: Contains claims or statements about an entity (typically the user) and additional data. Common claims include user ID, username, and expiration time.
- Signature: Ensures that the sender of the JWT is who they claim to be and that the message has not been altered.
Tokens like JWTs are typically not stored in the database along with other user details. Instead, they are issued by the server during the authentication process and then stored on the client side (e.g., in cookies or local storage) for later use.
To start using JWT, install the jsonwebtoken
package:
npm i jsonwebtoken
Now, update the auth-controller.js
file to generate and send a JWT when a user registers:
const User = require("../models/user-model");
const bcrypt = require("bcryptjs");
const register = async (req, res) => {
try {
const { username, email, phone, password } = req.body;
if (!username || !email || !phone || !password) {
return res.status(400).json({ message: "Please fill all the fields" });
}
const userExists = await User.findOne({ email: email });
if (userExists) {
return res.status(400).json({ message: "User already exists" });
}
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(String(password), saltRounds);
// Create a new user
const user = await User.create({
username,
email,
phone,
password: hashedPassword,
});
// Generate JWT Token
const token = await user.generateToken();
res.status(201).json({
message: "User registered successfully",
createdUser: user,
token: token,
userId: user._id.toString(),
});
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server error" });
}
};
module.exports = { register };
Next, head over to user-model.js
and add a method to generate a JWT:
const mongoose = require("mongoose");
const jwt = require("jsonwebtoken");
const userSchema = new mongoose.Schema({
username: { type: String, required: true },
email: { type: String, required: true, unique: true },
phone: { type: Number, required: true },
password: { type: String, required: true },
isAdmin: { type: Boolean, default: false },
});
userSchema.methods.generateToken = function () {
try {
const token = jwt.sign(
{ _id: this._id, email: this.email, isAdmin: this.isAdmin },
process.env.JWT_SECRET,
{ expiresIn: "7d" }
);
return token;
} catch (error) {
console.error(error);
return null;
}
};
const User = mongoose.model("User", userSchema);
module.exports = User;
Finally, add your JWT secret key to your .env
file:
JWT_SECRET=your_secret_key
Note : A secret key is a unique string used to sign and verify JWTs, ensuring their authenticity and integrity. It protects against unauthorized access by allowing the server to confirm that the token hasn't been tampered with.
After these changes, you can register a user via Postman. The response should include a JWT token, which you can use for authentication in future requests.
By using JSON Web Tokens (JWT), you've added an extra layer of security to your user authentication process. JWTs are an effective and scalable solution for managing user sessions in modern web applications.
To implement user login functionality, add the following route in auth-router.js
:
router.route("/login").post(login);
In auth-controller.js
, define the login
function:
const login = async (req, res) => {
try {
const { email, password } = req.body;
// Check if any field is empty
if (!email || !password) {
return res.status(400).json({ message: "Please fill all the fields" });
}
// Check if the user exists
const user = await User.findOne({ email });
if (!user) {
return res.status(400).json({ message: "Invalid Credentials" });
}
// Compare the password
const isPasswordValid = await bcrypt.compare(
String(password),
user.password
);
if (!isPasswordValid) {
return res
.status(401)
.json({ message: "Username or Password is Incorrect" });
}
// Successful login, return JWT
res.status(200).json({
message: "User logged in successfully",
token: await user.generateToken(),
userId: user._id.toString(),
});
} catch (error) {
console.error(error);
res.status(500).json({ message: "Server error. Please try again later." });
}
};
module.exports = { home, register, login };
Today, weβre focusing on implementing validation using Zod, a TypeScript-first schema declaration and validation library. Zod is designed to provide an easy-to-use and powerful way to validate data in JavaScript and TypeScript projects. It allows you to define schemas for your data and validate them with a simple API.
Zod is a schema validation library that provides a type-safe way to validate and parse data. It supports a wide range of validation features, including string, number, array, object validation, and more. Zod is particularly useful when you want to ensure that data conforms to a specific structure or set of rules, making it ideal for validating request bodies, query parameters, and other inputs in your applications.
- Define Schemas: You define schemas that describe the expected shape and constraints of your data.
- Parse and Validate: You use these schemas to parse and validate incoming data. If the data does not meet the schemaβs requirements, Zod throws a detailed error.
- Handle Errors: You catch and handle validation errors to provide meaningful feedback to users.
First, install Zod in your project:
npm i zod
Create a folder named validator
in the backend directory. Inside this folder, create a file named auth-validator.js
with the following content:
const { z } = require("zod");
const SignUpSchema = z.object({
username: z
.string({ required_error: "Username is required" })
.trim()
.min(3, { message: "Username must be at least 3 characters long" })
.max(255, { message: "Username must be at most 255 characters long" }),
email: z
.string({ required_error: "Email is required" })
.trim()
.min(3, { message: "Email must be at least 3 characters long" })
.max(255, { message: "Email must be at most 255 characters long" })
.email({ message: "Invalid email address", tldWhitelist: ["com", "net"] }),
phone: z
.string({ required_error: "Phone is required" })
.trim()
.min(10, { message: "Phone must be at least 10 characters long" })
.max(15, { message: "Phone must be at most 15 characters long" })
.regex(/^\d+$/, { message: "Phone must contain only digits" }),
password: z
.string({ required_error: "Password is required" })
.min(6, { message: "Password must be at least 6 characters long" })
.max(255, { message: "Password must be at most 255 characters long" }),
});
module.exports = { SignUpSchema };
Create a folder named middlewares
and add a file named validate-middleware.js
:
const validate = (Schema) => async (req, res, next) => {
try {
const parsedBody = Schema.parse(req.body); // Validate the request body
req.body = parsedBody; // Overwrite the request body with the parsed data
next(); // Move to the next middleware or route handler
} catch (error) {
res.status(400).json({
message: "Validation Failed",
errors: error.errors, // Detailed errors from Zod
});
}
};
module.exports = validate;
Integrate the validation middleware into your routes. Open auth-router.js
and update it as follows:
const express = require("express");
const router = express.Router();
const { home, register, login } = require("../controllers/auth-controller");
const { SignUpSchema } = require("../validators/auth-validator");
const validate = require("../middlewares/validate-middleware");
router.route("/").get(home);
router.route("/register").post(validate(SignUpSchema), register); // Add validation middleware here
router.route("/login").post(login);
module.exports = router;
By following these steps, youβve integrated Zod validation into your Express application. This setup ensures that incoming data is validated according to the defined schema, providing robust error handling and improving data integrity in your application.
Todayβs focus was on implementing centralized error handling in an Express.js application using a custom error middleware. This approach allows you to manage all errors from a single place, enhancing code maintainability and readability.
- Error Middleware: A special type of middleware in Express that handles errors thrown in the application, simplifying error management.
- Centralized Error Handling: Allows catching and responding to errors from a single location, improving maintainability.
-
Create the Error Middleware:
In the
middlewares
folder, create a file namederror-middleware.js
:const errorMiddleware = (err, req, res, next) => { const status = err.status || 500; const message = err.message || "Something went wrong. Server error. Please try again later."; return res.status(status).json({ message }); }; module.exports = errorMiddleware;
-
Modify Your Controllers:
Replace
catch
blocks withnext(error)
to delegate error handling to the middleware. For example, inauth-controller.js
:const register = async (req, res, next) => { try { const { username, email, phone, password } = req.body; if (!username || !email || !phone || !password) { return res.status(400).json({ message: "Please fill all the fields" }); } const userExists = await User.findOne({ email }); if (userExists) { return res.status(400).json({ message: "User already exists" }); } const saltRound = 10; const hashedPassword = await bcrypt.hash(String(password), saltRound); const user = await User.create({ username, email, phone, password: hashedPassword, }); res.status(201).json({ message: "User registered successfully", createdUser: user, token: await user.generateToken(), userId: user._id.toString(), }); } catch (error) { next(error); // Pass the error to the middleware } };
-
Update the Validate Middleware:
Update the validation middleware to send errors to the error middleware:
const validate = (Schema) => async (req, res, next) => { try { const parsedBody = Schema.parse(req.body); req.body = parsedBody; next(); } catch (error) { next({ status: 400, message: error.errors, }); } }; module.exports = validate;
-
Integrate the Middleware:
Finally, make sure to include the error middleware in
index.js
:require("dotenv").config(); const express = require("express"); const app = express(); const router = require("./router/auth-router"); const connectDB = require("./utils/db"); const errorMiddleware = require("./middlewares/error-middleware"); app.use(express.json()); app.use("/api/auth", router); app.get("/", (req, res) => { res.send("Hello World"); }); app.use(errorMiddleware); // This must be just above the connection connectDB() .then(() => { const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); }) .catch((err) => { console.error("Database connection failed", err); process.exit(1); });
By setting up a centralized error handling middleware in Express, you can efficiently manage errors across your application, improving the reliability and readability of your code.
Todayβs focus was on implementing a contact form in an Express.js application. This included creating a Mongoose schema, setting up routes, and defining the logic for handling form submissions.
- Mongoose Schema: Defines the structure of the data for MongoDB.
- Express Routes: Handles incoming requests and integrates with controllers.
- Error Handling: Manages and logs errors during form submission.
Create a schema for the contact form data in models/contact-form-model.js
:
const mongoose = require("mongoose");
const contactFormSchema = new mongoose.Schema({
email: {
type: String,
required: true,
},
subject: {
type: String,
required: true,
},
message: {
type: String,
required: true,
},
});
const ContactForm = mongoose.model("ContactForm", contactFormSchema);
module.exports = ContactForm;
Note: Ensure that the email
field is not unique if you want to allow multiple submissions with the same email address.
Implement the logic to handle form submissions in controllers/contact-controller.js
:
const Contact = require("../models/contact-form-model");
const contactForm = async (req, res, next) => {
try {
console.log(req.body);
const { email, subject, message } = req.body;
// Check if any field is empty
if (!email || !subject || !message) {
return res.status(400).json({ message: "Please fill all the fields" });
}
// Create a new form data
const contactData = await Contact.create({
email,
subject,
message,
});
res.status(201).json({
message: "Form Submitted Successfully",
formData: contactData,
});
console.log(contactData);
} catch (error) {
console.error(error);
next(error); // Pass the error to the middleware
}
};
module.exports = { contactForm };
Define the route to handle contact form submissions in router/contact-router.js
:
const express = require("express");
const router = express.Router();
const { contactForm } = require("../controllers/contact-controller");
const { ContactFormSchema } = require("../validators/contact-form-validator");
const validate = require("../middlewares/validate-middleware");
router.route("/contact").post(validate(ContactFormSchema), contactForm); // Middleware to validate the request body
module.exports = router;
Implement the validation logic for the contact form data in validators/contact-form-validator.js
:
const { z } = require("zod");
const ContactFormSchema = z.object({
email: z
.string({ required_error: "Email is required" })
.trim()
.min(3, { message: "Email must be at least 3 characters long" })
.max(255, { message: "Email must be at most 255 characters long" })
.email({ message: "Invalid email address", tldWhitelist: ["com", "net"] }),
subject: z
.string({ required_error: "Subject is required" })
.trim()
.min(3, { message: "Subject must be at least 3 characters long" })
.max(255, { message: "Subject must be at most 255 characters long" }),
message: z
.string({ required_error: "Message is required" })
.trim()
.min(3, { message: "Message must be at least 3 characters long" })
.max(255, { message: "Message must be at most 255 characters long" }),
});
module.exports = { ContactFormSchema };
Ensure the contact route is integrated into your main application file index.js
:
require("dotenv").config();
const express = require("express");
const app = express();
const authRoute = require("./router/auth-router");
const contactRoute = require("./router/contact-router");
const connectDB = require("./utils/db");
const errorMiddleware = require("./middlewares/error-middleware");
// Middleware
app.use(express.json());
app.use("/api/auth", authRoute);
app.use("/api/form", contactRoute);
app.get("/", (req, res) => {
res.send("Hello World");
});
app.use(errorMiddleware); // This must be just above the connection
// Connect to the database and start the server
connectDB()
.then(() => {
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
})
.catch((err) => {
console.error("Database connection failed", err);
process.exit(1);
});
To test the contact form, use Postman to send a POST request to http://localhost:3000/api/form/contact
with the following JSON body:
{
"email": "example@example.com",
"subject": "Test Subject",
"message": "This is a test message."
}
In this setup, we created a contact form feature that includes a Mongoose schema, an Express route, and validation logic. The contact form allows users to submit their email, subject, and message, and the data is stored in MongoDB.
This centralized approach to handling form submissions, including validation and error management, helps maintain clean and organized code.
-
Navigate to the frontend directory:
cd frontend
-
Create a new React project using Vite:
npm create vite@latest .
-
Configuration:
- Select a framework: React
- Select a variant: JavaScript
-
Install dependencies and start the development server:
npm install npm run dev
-
Install Tailwind CSS and its dependencies:
npm install -D tailwindcss postcss autoprefixer npx tailwindcss init -p
-
Configure Tailwind in
tailwind.config.js
:/** @type {import('tailwindcss').Config} */ export default { content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], theme: { extend: {}, }, plugins: [], };
-
Update
index.css
to use Tailwindβs base, components, and utilities:@tailwind base; @tailwind components; @tailwind utilities;
-
Update
App.jsx
to include a test component:import "./App.css"; const App = () => { return ( <> <h1 className="text-3xl font-bold underline">Hello world!</h1> </> ); }; export default App;
-
Run the development server to verify the setup:
npm run dev
After following these steps, you should see "Hello world!" styled with Tailwind CSS on your browser, indicating that your ReactJS environment is successfully set up.
Today, we're learning how to create a multi-page application in React using React Router DOM. This will allow us to navigate between different pages without reloading the browser.
- React Router DOM basics: How to set up and use React Router for page navigation.
- Creating a simple layout: Adding a Header, Footer, and dynamic content area.
- Routing in React: Displaying different components based on the URL path.
Letβs break it down step by step!
First, we need to install react-router-dom
, a package that handles routing in React applications.
Open your terminal and run:
npm install react-router-dom
This command installs the necessary library, enabling us to set up navigation between different pages in our React app.
We'll create three simple pages: Home, About, and Contact. These will be the different pages that users can navigate to.
- Inside the
src
directory, create a new folder namedpages
. - In the
pages
folder, create three files:Home.jsx
,About.jsx
, andContact.jsx
.
Home.jsx:
const Home = () => {
return (
<div>
<h1 className="m-3">Home Page</h1>
</div>
);
};
export default Home;
About.jsx:
const About = () => {
return (
<div>
<h1 className="m-3">About Page</h1>
</div>
);
};
export default About;
Contact.jsx:
const Contact = () => {
return (
<div>
<h1 className="m-3">Contact Page</h1>
</div>
);
};
export default Contact;
- Each file contains a simple functional component. A functional component is just a JavaScript function that returns some JSX (HTML-like syntax).
- Weβre keeping the pages simple with just a heading for now. The goal is to understand routing.
Now, letβs create a navigation menu (Header) and a Footer that will appear on every page.
- Inside the
src
directory, create another folder namedcomponents
. - In the
components
folder, create two files:Header.jsx
andFooter.jsx
.
Header.jsx:
// Importing NavLink from React Router to create navigation links
import { NavLink } from "react-router-dom";
const Header = () => {
return (
<header className="w-full">
<nav className="bg-white text-lg">
<ul className="flex font-medium">
<li className="m-3">
<NavLink
to="/"
className={({ isActive }) =>
`py-2 ${isActive ? "text-orange-700" : "text-gray-700"}`
}
>
Home
</NavLink>
</li>
<li className="m-3">
<NavLink
to="/about"
className={({ isActive }) =>
`py-2 ${isActive ? "text-orange-700" : "text-gray-700"}`
}
>
About
</NavLink>
</li>
<li className="m-3">
<NavLink
to="/contact"
className={({ isActive }) =>
`py-2 ${isActive ? "text-orange-700" : "text-gray-700"}`
}
>
Contact
</NavLink>
</li>
</ul>
</nav>
</header>
);
};
export default Header;
- NavLink: This component works like an anchor (
<a>
) tag but with additional features from React Router. It helps you create navigation links that are aware of the active route. - Dynamic Styling: The function inside
className
checks if the link is active. If it is, the link text turns orange; otherwise, it remains gray.
Footer.jsx:
const Footer = () => {
return <div className="m-3">Footer</div>;
};
export default Footer;
The Footer.jsx
component is simple and straightforward, just displaying a footer message.
The App.jsx
file will act as our main layout. It will include the Header, Footer, and a dynamic content area where different pages will be displayed.
Replace the existing content in App.jsx
with the following:
// Importing outlet
import { Outlet } from "react-router-dom";
import Header from "./components/Header";
import Footer from "./components/Footer";
import "./App.css";
const App = () => {
return (
<>
{/* Header at the top of every page */}
<Header />
{/* Outlet to render the current page */}
<Outlet />
{/* Footer at the bottom of every page */}
<Footer />
</>
);
};
export default App;
- Header & Footer: These components are included so they appear on every page.
- Outlet: This is a placeholder where the current page content (like Home, About, or Contact) will be displayed based on the active route.
Now, letβs set up our routes to connect the URLs with the pages weβve created.
Replace the existing content in main.jsx
with the following:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
// Import necessary modules
import {
Route,
RouterProvider,
createBrowserRouter,
createRoutesFromElements,
} from "react-router-dom";
import Home from "./pages/Home.jsx";
import About from "./pages/About.jsx";
import Contact from "./pages/Contact.jsx";
// Define the routes for the application
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<App />}>
{/* Home route */}
<Route path="" element={<Home />} />
{/* About route */}
<Route path="/about" element={<About />} />
{/* Contact route */}
<Route path="/contact" element={<Contact />} />
</Route>
)
);
createRoot(document.getElementById("root")).render(
<StrictMode>
{/* Provide the router configuration to the application */}
<RouterProvider router={router} />
</StrictMode>
);
- Route Configuration: The
Route
component connects each URL path to a specific component. For example, when the user navigates to/about
, theAbout
component is displayed. - Nested Routes: The
App
component serves as a parent route, soHeader
andFooter
are always shown, and theOutlet
dynamically loads the current page based on the route.
Now that everything is set up, letβs test it out!
-
Start the Development Server:
npm run dev
This command starts the React development server.
-
Open the Browser:
- Navigate to
http://localhost:3000/
in your web browser. You should see the Home page.
- Navigate to
-
Navigate Between Pages:
- Click on the "About" link in the Header to navigate to the About page.
- Click on the "Contact" link to navigate to the Contact page.
- Notice how the URL changes, but the page doesnβt reloadβthis is the magic of React Router!
Well done! π Youβve successfully created a multi-page React application with seamless navigation. Here's a quick recap of what you learned today:
- React Router DOM Basics: You learned how to install and set up routing in a React app.
- Creating a Layout: You built a consistent layout with a Header and Footer across all pages.
- Dynamic Navigation: You used
NavLink
to highlight the current page and make navigation intuitive.
This knowledge is crucial for building real-world React applications. Keep experimenting with routing, and soon you'll be building even more dynamic and complex apps! π
For more knowledge about React Router DOM, do checkout this repo : React Router Crash Course
In this lesson, we'll build a simple registration form in React JS and add navigation for the form. This tutorial will guide you step-by-step, making sure that even if youβre new to React, youβll understand each part clearly.
We start by adding a new navigation link that points to our registration page. This will allow users to easily navigate to the registration form from the website's header.
Add a new NavLink for the /register
page:
<NavLink
to="/register"
className={({ isActive }) =>
`py-2 ${isActive ? "text-blue-400" : "text-gray-300"} hover:text-blue-400`
}
>
Register
</NavLink>
NavLink
Component: Used to create navigation links in React. Theto
prop specifies the URL path, and theclassName
applies styles dynamically based on whether the link is active.- Dynamic Styling: The
className
usesisActive
to highlight the link when the user is on the corresponding page.
Next, we need to define a route for our new registration page so that React Router knows what to display when the user navigates to /register
.
Add the route for the registration page:
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<App />}>
<Route path="" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/register" element={<Register />} /> {/* Route for Register Page */}
</Route>
)
);
- Route Setup: Weβre telling React Router to render the
Register
component when the user visits/register
.
Letβs now create the registration page component. This component will contain a form where users can input their details like username, email, phone number, and password.
Inside the pages
folder, create a new file named Register.jsx
:
import { useState } from "react";
const Register = () => {
// State to manage form data
const [formData, setFormData] = useState({
username: "",
email: "",
phone: "",
password: "",
});
// Handle input changes
const handleInput = (e) => {
const name = e.target.name; // Name of the input field
const value = e.target.value; // Value of the input field
// Update the formData state
setFormData({
...formData,
[name]: value,
});
};
// Handle form submission
const handleSubmit = (e) => {
e.preventDefault(); // Prevent default form submission behavior
console.log("Form submitted:", formData);
// Here we will write logic to store data in backend
};
return (
<>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username</label>
<input
type="text"
name="username" // Field identifier
id="username"
value={formData.username} // State value
onChange={handleInput} // Update value
placeholder="Enter Username"
required
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
name="email"
id="email"
value={formData.email}
onChange={handleInput}
placeholder="Enter Email"
required
/>
</div>
<div>
<label htmlFor="phone">Phone</label>
<input
type="text"
name="phone"
id="phone"
value={formData.phone}
onChange={handleInput}
placeholder="Enter Phone"
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
id="password"
value={formData.password}
onChange={handleInput}
placeholder="Enter Password"
required
/>
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
</>
);
};
export default Register;
- State Management with
useState
: We use theuseState
hook to manage the form data, including the username, email, phone number, and password. - Input Handling: The
handleInput
function captures changes to the input fields and updates the state accordingly. - Form Submission: The
handleSubmit
function prevents the default form submission behavior and logs the form data to the console. In a real application, youβd replace this with logic to send the data to a backend server.
Youβve successfully created a registration form! Now, as a challenge, try creating a login form on your own. Hereβs what you need to do:
- Add a NavLink for
/login
inHeader.jsx
. - Create a
Login.jsx
component with form fields for email and password. - Add a route for
/login
inmain.jsx
.
Reference Design
This guide should help you create a fully functional registration form with navigation. With React, once you understand the basic concepts like state management, routing, and component structure, you can build even more complex applications with ease!
Today, we are going to implement a basic 404 error page in our React application. This page will be displayed whenever a user tries to access a route that doesn't exist.
First, we need to handle routes that don't match any of our defined paths by adding a wildcard (*
) route in main.jsx
.
import Error from "./pages/Error.jsx"; // Import the Error page
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<App />}>
<Route path="" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
<Route path="*" element={<Error />} /> {/* Catch-all route for 404 */}
</Route>
)
);
Next, we'll create a simple Error.jsx
component in the pages
folder. This component will display a "404 Not Found" message and a link to return to the homepage.
import { Link } from "react-router-dom";
const Error = () => {
return (
<>
<h1>404 Not Found</h1>
<p>Sorry, the page you're looking for doesn't exist.</p>
<Link to="/">Go back to the main page</Link>{" "}
{/* Link to redirect user to home */}
</>
);
};
export default Error;
-
Wildcard Route (
*
): The*
in the route path acts as a catch-all. Any path that doesn't match the defined routes will fall back to this route, triggering theError
component. -
Error Component: The
Error.jsx
file is a simple functional component that renders a message informing the user that the page was not found and includes a link to return to the homepage.
Now, if you try to access a route in your application that doesn't exist (e.g., http://localhost:3000/some-random-route
), the 404 error page will be displayed.
This setup ensures a smooth user experience by guiding users back to a valid part of your application if they land on an incorrect URL.
On Day 17, weβll connect our frontend (React) to the backend (Express.js) to store user registration data in MongoDB. Weβll cover setting up the connection, handling CORS errors, and successfully sending data from the frontend to the backend.
Ensure you have two terminals open in VS Code:
- Start the frontend with
npm run dev
. - Start the backend with
nodemon index.js
.
In the previous steps, we tested our backend using Postman to store data in MongoDB. Now, weβll connect the frontend to the backend to handle the same functionality using a registration form in React.
-
Frontend
.env
File: In the frontend folder, create a.env
file to store the backend URL.VITE_BACKEND_URL=http://localhost:3000
-
Backend
.env
File: In the backend folder, update the.env
file to include the frontend URL and other environment variables.PORT=3000 MONGO_URI=mongodb+srv://<username>:<password>@cluster1.o4g0r.mongodb.net JWT_SECRET=amansecretkey VITE_FRONTEND_URL=http://localhost:5173
Weβll update the handleSubmit
function in Register.jsx
to send the registration data to our backend.
import { useState } from "react";
import { useNavigate } from "react-router-dom";
const Register = () => {
const [formData, setFormData] = useState({
username: "",
email: "",
phone: "",
password: "",
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData({
...formData,
[name]: value,
});
};
const navigate = useNavigate();
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL; // Getting the backend URL from the .env file
const handleSubmit = async (e) => {
e.preventDefault();
console.log("Form submitted:", formData);
try {
const response = await fetch(`${BACKEND_URL}/api/auth/register`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData), // Convert JS object to JSON string
});
const data = await response.json();
console.log(data);
// Clearing the form after submission
if (response.ok) {
setFormData({
username: "",
email: "",
phone: "",
password: "",
});
alert("Registration successful");
navigate("/login");
} else {
alert("Registration failed");
}
} catch (error) {
console.error("Error:", error);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username</label>
<input
type="text"
name="username"
id="username"
value={formData.username}
onChange={handleChange}
placeholder="Enter Username"
required
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
name="email"
id="email"
value={formData.email}
onChange={handleChange}
placeholder="Enter Email"
required
/>
</div>
<div>
<label htmlFor="phone">Phone</label>
<input
type="text"
name="phone"
id="phone"
value={formData.phone}
onChange={handleChange}
placeholder="Enter Phone"
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
id="password"
value={formData.password}
onChange={handleChange}
placeholder="Enter Password"
required
/>
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
);
};
export default Register;
When connecting the frontend with the backend, you might encounter a CORS Policy Error. This occurs because web browsers restrict cross-origin HTTP requests.
CORS (Cross-Origin Resource Sharing) is a security feature that allows or restricts web pages from making requests to a different domain. In a MERN stack application, this issue arises when the frontend and backend are hosted on different domains.
To resolve the CORS issue, install the CORS package in the backend.
-
Install CORS: Ensure you're in the backend directory and run the following command:
npm i cors
-
Configure CORS in
index.js
: Add the following code to yourindex.js
file:require("dotenv").config(); const express = require("express"); const cors = require("cors"); // Import cors const app = express(); const authRoute = require("./router/auth-router"); const contactRoute = require("./router/contact-router"); const connectDB = require("./utils/db"); const errorMiddleware = require("./middlewares/error-middleware"); // CORS options for cross-origin requests const corsOptions = { origin: process.env.VITE_FRONTEND_URL, // Frontend URL from .env optionsSuccessStatus: 200, methods: "GET,HEAD,PUT,PATCH,POST,DELETE", credentials: true, }; app.use(cors(corsOptions)); // Use cors with defined options app.use(express.json()); app.use("/api/auth", authRoute); // Auth routes app.use("/api/form", contactRoute); // Contact form routes app.get("/", (req, res) => { res.send("Hello World"); }); app.use(errorMiddleware); // Use error middleware // Connect to the database and start the server connectDB() .then(() => { const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); }) .catch((err) => { console.error("Database connection failed", err); process.exit(1); });
After making the changes, you should be able to register users through the frontend. Here's what to expect:
To streamline the process of starting your backend server, add a start
script to the package.json
file inside the backend folder.
Open your package.json
file in the backend folder and add the following start
script under "scripts"
:
{
"name": "backend",
"version": "1.0.0",
"description": "server",
"main": "index.js",
"scripts": {
"start": "node index.js" // Add this line
},
"author": "aman",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.8.0",
"mongoose": "^8.5.2",
"zod": "^3.23.8"
}
}
Now, you can start your backend server by running the following command:
npm start
This command will work in both development and production environments.
In addition to storing registration data, extend the functionality to store contact form data in MongoDB. Follow similar steps as you did for the registration form.
By the end of this task, you should be able to store both registration and contact form data in MongoDB from the frontend.
On Day 18, we'll add login functionality to the frontend using React and connect it to the backend for user authentication.
- Backend should be running (
nodemon index.js
). - Frontend should be running (
npm run dev
).
Make sure the login route is added in main.jsx
:
<Route path="/login" element={<Login />} />
Focus on the handleSubmit
function to send the login request to the backend. Here's the essential code:
import { useState } from "react";
import { useNavigate } from "react-router-dom";
const Login = () => {
const [formData, setFormData] = useState({
email: "",
password: "",
});
const navigate = useNavigate();
const handleInput = (e) => {
const name = e.target.name;
const value = e.target.value;
setFormData({
...formData,
[name]: value,
});
};
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL; // Getting the backend URL(localhost:3000) from the .env file
const handleSubmit = async (e) => {
e.preventDefault();
console.log("Form submitted:", formData);
try {
const response = await fetch(`${BACKEND_URL}/api/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const data = await response.json();
console.log(data);
if (response.ok) {
setFormData({ email: "", password: "" });
navigate("/");
} else {
alert("Login failed, please try again.");
}
} catch (error) {
console.error("Error:", error);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Email</label>
<input
type="email"
name="email"
id="email"
value={formData.email}
onChange={handleInput}
placeholder="Enter Email"
required
/>
</div>
<div>
<label htmlFor="password">Password</label>
<input
type="password"
name="password"
id="password"
value={formData.password}
onChange={handleInput}
placeholder="Enter Password"
required
/>
</div>
<button type="submit">Login</button>
</form>
);
};
export default Login;
- Successful Login: Redirects to the homepage.
- Login Error: Displays an alert to the user.
If you encounter CORS issues, ensure CORS is set up in the backend (handled on Day 17).
The backend logic for handling login requests is already implemented.
You now have a functional login page that communicates with the backend to authenticate users.
On Day 19, we'll focus on securely saving a JWT (JSON Web Token) in the browser's local storage to keep users logged in. We'll use React's Context API to make it easier to manage authentication across the app.
- Create an authentication context to store the token.
- Wrap your app with the context so it can be used anywhere.
- Update the Login and Register pages to store the token after successful login or registration.
First, we'll create a new context to store and access the JWT token.
- Create a folder called
Store
inside thesrc
directory. - Inside the
Store
folder, create a file namedauth.jsx
. - Add the following code:
import React, { createContext, useContext } from "react";
// Step 1: Create a context
export const AuthContext = createContext();
// Step 2: Create a provider to share the token
export const AuthProvider = ({ children }) => {
const storeTokenInLocalStorage = (serverToken) => {
localStorage.setItem("token", serverToken); // Save the token in local storage
};
return (
<AuthContext.Provider value={{ storeTokenInLocalStorage }}>
{children} {/* All components inside will have access to this */}
</AuthContext.Provider>
);
};
// Step 3: Create a custom hook to use the context easily
export const useAuth = () => {
return useContext(AuthContext);
};
Explanation:
- Context allows us to share data (like a token) between components without passing it manually every time.
- The
AuthProvider
provides the functionstoreTokenInLocalStorage
to save the token in local storage. - The
useAuth
hook makes it easier to use this function in other parts of the app.
Next, we need to make this authentication context available to the entire app.
- Open
main.jsx
. - Wrap your app in the
AuthProvider
component, like this:
import { AuthProvider } from "./store/auth";
createRoot(document.getElementById("root")).render(
<AuthProvider>
{" "}
{/* Wrap the app to give access to the auth context */}
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
</AuthProvider>
);
Now, weβll update the Login and Register pages so they can store the token after a successful login or registration.
- Open
Login.jsx
(and similarly forRegister.jsx
). - Modify the file to save the token when the login/registration is successful:
import { useAuth } from "../store/auth"; // Import the custom hook to access auth context
const { storeTokenInLocalStorage } = useAuth(); // Get the function to store token
const data = await response.json(); // Get the response data (which includes the token)
if (response.ok) {
storeTokenInLocalStorage(data.token); // Save the token in local storage
setFormData({
email: "",
password: "",
});
alert("Login successful");
navigate("/"); // Redirect to the homepage
} else {
alert("Login failed, please try again.");
}
Whatβs Happening Here:
- After a successful login or registration, the JWT token is returned by the server.
- We use
storeTokenInLocalStorage(data.token)
to save the token in local storage. - This ensures the user stays logged in, even after refreshing the page.
You can verify that the JWT token is being stored in the browser by:
- Opening your app.
- Logging in or registering.
- Inspecting the browser's local storage (Right-click -> Inspect -> Application tab -> Local Storage).
Hereβs how it should look:
After completing Day 19:
- You now have a system to save JWT tokens in the browser.
- The token is stored securely in local storage using the Context API.
- Your app can now keep users logged in across different pages and sessions.
This process is essential for authentication in modern web apps and will help keep your users logged in securely!
Today, weβll learn how to log users out of the application by removing the JWT token from local storage. This ensures that when users log out, they are no longer authenticated.
- Create a Logout route in
main.jsx
. - Build a Logout.jsx component to handle logging out.
- Update the AuthContext to include the logout functionality.
- Modify the Header.jsx to display the Logout link when the user is logged in.
To start, we need to create a route for logging out.
- Open
main.jsx
. - Add a new route for the Logout page:
const router = createBrowserRouter(
createRoutesFromElements(
<Route path="/" element={<App />}>
<Route path="" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/register" element={<Register />} />
<Route path="/login" element={<Login />} />
<Route path="/logout" element={<Logout />} /> {/* Logout route */}
<Route path="*" element={<Error />} />
</Route>
)
);
This creates a route that points to the Logout component.
Next, weβll create the component that handles logging the user out by clearing the token from local storage.
- Inside your
pages
folder, create a file calledLogout.jsx
. - Add the following code:
import { useEffect } from "react";
import { Navigate } from "react-router-dom";
import { useAuth } from "../store/auth";
const Logout = () => {
const { LogoutUser } = useAuth(); // Get the logout function from context
useEffect(() => {
LogoutUser(); // Call logout function when the component loads
}, [LogoutUser]);
return <Navigate to="/login" />; // Redirect the user to the login page
};
export default Logout;
Explanation:
- The
LogoutUser
function is called as soon as the component loads, removing the JWT token. - After logging out, the user is immediately redirected to the Login page.
We need to modify the authentication context to support the logout functionality.
- Open
auth.jsx
(inside yourstore
folder). - Add the following updates:
import { createContext, useContext, useState } from "react";
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(localStorage.getItem("token")); // Get token from local storage
let isLoggedIn = !!token; // Check if the user is logged in
// Function to store the token
const storeTokenInLocalStorage = (serverToken) => {
setToken(serverToken);
localStorage.setItem("token", serverToken); // Save token to local storage
};
// Logout function to clear the token
const LogoutUser = () => {
setToken(""); // Clear the token in state
localStorage.removeItem("token"); // Remove the token from local storage
};
return (
<AuthContext.Provider
value={{ storeTokenInLocalStorage, LogoutUser, isLoggedIn }}
>
{children}
</AuthContext.Provider>
);
};
// Custom hook to use the AuthContext
export const useAuth = () => {
return useContext(AuthContext);
};
What Changed:
- We added a
LogoutUser
function that clears the token from both state and local storage. isLoggedIn
now dynamically checks if a token is present.
We need to make sure the Logout link is only shown when the user is logged in.
- Open
Header.jsx
. - Modify the component like this:
import { useAuth } from "../store/auth"; // Import the AuthContext
const Header = () => {
const { isLoggedIn } = useAuth(); // Check if the user is logged in
return (
<header>
<nav>
<ul>
{isLoggedIn ? ( // Show Logout link if user is logged in
<li>
<NavLink
to="/logout"
className={({ isActive }) =>
`py-2 ${
isActive ? "text-blue-400" : "text-gray-300"
} hover:text-blue-400`
}
>
Logout
</NavLink>
</li>
) : (
<>
<li>
<NavLink
to="/register"
className={({ isActive }) =>
`py-2 ${
isActive ? "text-blue-400" : "text-gray-300"
} hover:text-blue-400`
}
>
Register
</NavLink>
</li>
<li>
<NavLink
to="/login"
className={({ isActive }) =>
`py-2 ${
isActive ? "text-blue-400" : "text-gray-300"
} hover:text-blue-400`
}
>
Login
</NavLink>
</li>
</>
)}
</ul>
</nav>
</header>
);
};
export default Header;
Explanation:
- If the user is logged in (
isLoggedIn
istrue
), show the Logout link. - If the user is not logged in, show the Login and Register links.
Now, users can securely log out of your app!
Today, weβll create a JWT Token Verification Middleware to protect routes and allow only authenticated users to access certain endpoints. Additionally, weβll create a route to get user data from the database based on their JWT token.
First, we will add a protected route to fetch user data. This route will use the JWT Token Verification Middleware to ensure only logged-in users can access it.
const express = require("express");
const router = express.Router();
const {
home,
register,
login,
user,
} = require("../controllers/auth-controller");
const { SignUpSchema, LoginSchema } = require("../validators/auth-validator");
const validate = require("../middlewares/validate-middleware");
const authMiddleware = require("../middlewares/auth-middleware");
router.route("/").get(home);
router.route("/register").post(validate(SignUpSchema), register);
router.route("/login").post(validate(LoginSchema), login);
// Get User Data Route with authMiddleware protection
router.route("/user").get(authMiddleware, user);
module.exports = router;
This middleware checks for the token in the request headers, verifies it using the secret key, and fetches the user data from the database if the token is valid.
In the middlewares
folder, create auth-middleware.js
:
const jwt = require("jsonwebtoken");
const User = require("../models/user-model");
const authMiddleware = async (req, res, next) => {
try {
const token = req.header("Authorization");
// Check if token is provided
if (!token) {
return res
.status(401)
.json({ message: "Unauthorized User, Token Not Found" });
}
// Clean the token by removing 'Bearer'
const jwtToken = token.replace("Bearer", "").trim();
// Verify the token
const decoded = jwt.verify(jwtToken, process.env.JWT_SECRET);
// Find the user in the database using the decoded token data
const userData = await User.findOne({ email: decoded.email }).select({
password: 0,
});
if (!userData) {
return res
.status(404)
.json({ message: `User with email ${decoded.email} not found` });
}
// Attach user data and token to the request object
req.user = userData;
req.token = jwtToken;
req.userID = userData._id;
// Proceed to the next middleware or controller
next();
} catch (error) {
next({ status: 400, message: error.message });
}
};
module.exports = authMiddleware;
In auth-controller.js
, add the controller to handle fetching the user data once they are authenticated.
// Controller to send user data
const user = async (req, res, next) => {
try {
const userData = req.user;
return res.status(200).json({
user: userData,
message: "User data sent successfully",
});
} catch (error) {
console.error(error);
next(error);
}
};
-
URL:
http://localhost:3000/api/auth/user
-
Method:
GET
-
Authorization Header:
- Key:
Authorization
- Value:
Bearer <your_jwt_token>
(replace<your_jwt_token>
with the token stored in local storage after login).
Example:
Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2NmQ0MzRlNzg2NmJjMzExYmVhMjA0MjMiLCJlbWFpbCI6ImFtYW5AZ21haWwuY29tIiwiaXNBZG1pbiI6ZmFsc2UsImlhdCI6MTcyNzYwNTQ3OSwiZXhwIjoxNzI4MjEwMjc5fQ.-CADCRE4t4NGCeRM9KAhOx1orVWweM4jkUUIpUUX084
- Key:
Screenshot:
- We created a JWT Token Verification Middleware that protects routes by verifying the JWT token sent in the request headers.
- We added a user route to fetch user data from the database only for authenticated users.
- We tested the route using Postman by sending the JWT token in the Authorization header.
Now, you have a fully functional protected route for fetching user data! π
Today, we will learn how to fetch user details from the backend and display them on the frontend after the user has logged in using a JWT token.
In this step, weβll add a function in our auth.jsx
file that fetches user details from the backend using the stored JWT token.
import { createContext, useContext, useEffect, useState } from "react";
// Create the context
export const AuthContext = createContext();
// AuthProvider component (Provide the Context value)
export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(localStorage.getItem("token"));
const [loggedInUser, setLoggedInUser] = useState(""); // Store logged-in user data
let isLoggedIn = !!token; // Check if the token exists, user is logged in
const storeTokenInLocalStorage = (serverToken) => {
setToken(serverToken);
localStorage.setItem("token", serverToken); // Save token to local storage
};
// Logout function
const LogoutUser = () => {
setToken("");
return localStorage.removeItem("token");
};
// Get User Data from the backend
const getUserData = async () => {
try {
const backendURL = import.meta.env.VITE_BACKEND_URL; // Backend URL from environment
const url = `${backendURL}/api/auth/user`; // API endpoint for user data
const response = await fetch(url, {
method: "GET",
headers: {
Authorization: `Bearer ${token}`, // Send token with the request
},
});
if (response.ok) {
const data = await response.json(); // Get user data from the response
setLoggedInUser(data); // Set user data in state
} else {
console.log("Failed to Fetch Data");
}
} catch (error) {
console.log(error);
}
};
// Fetch user data once the component is mounted
useEffect(() => {
getUserData();
}, []);
return (
<AuthContext.Provider
value={{ storeTokenInLocalStorage, LogoutUser, isLoggedIn, loggedInUser }}
>
{children}
</AuthContext.Provider>
);
};
// Custom hook to use the Auth Context
export const useAuth = () => {
return useContext(AuthContext);
};
Now that the user data is fetched and stored in loggedInUser
, you can display it anywhere in your app, such as in the Header component or a Profile page.
Hereβs how you can access the user details in the Header.jsx
:
import { useAuth } from "../store/auth"; // Import the useAuth hook
const Header = () => {
const { loggedInUser } = useAuth(); // Get the user data from the Auth Context
return (
<header>
{loggedInUser?.user?.username && (
<p>Welcome, {loggedInUser.user.username}!</p> // Display the username
)}
</header>
);
};
export default Header;
- Login: First, log in as a user to get a valid JWT token stored in the browser's local storage.
- Verify Token: The
getUserData
function will automatically use this token to fetch the user's data from the backend. - View Data: You should now see the logged-in userβs username (or any other data) displayed in the component where you accessed
loggedInUser
.
- We updated the
auth.jsx
file to include a function for fetching user details from the backend using the JWT token stored in local storage. - We used the
useAuth
hook to access the user data and display it in the frontend, such as in theHeader
or other components. - The userβs details will now be shown after a successful login.
Now, you can easily display user information anywhere in your app after they have logged in! π
Today, we'll integrating React Toastify to display success and error messages in our Login and Registration forms.
In your frontend project folder, install react-toastify
:
npm i react-toastify
Import ToastContainer
and its CSS to make Toastify available across the app. Add <ToastContainer />
to the top-level component structure in main.jsx
.
Example changes in main.jsx
:
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from 'react-toastify';
// Wrap App with ToastContainer and AuthProvider
createRoot(document.getElementById("root")).render(
<AuthProvider>
<ToastContainer /> {/* This enables toast notifications */}
<StrictMode>
<RouterProvider router={router} />
</StrictMode>
</AuthProvider>
);
In the Login.jsx
component, import and use Toastify to display feedback messages.
Example steps in Login.jsx
:
- Import Toastify: Import
toast
fromreact-toastify
. - Handle Success and Error: Inside
handleSubmit
, calltoast.success()
ortoast.error()
based on the response.
import { toast } from "react-toastify";
// Handle form submission
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await fetch(`${BACKEND_URL}/api/auth/login`, { /* config */ });
const data = await response.json();
if (response.ok) {
storeTokenInLocalStorage(data.token); // Save token
toast.success("Login successful"); // Success message
navigate("/");
} else {
toast.error(data.message[0]?.message || "Login failed"); // Error message
}
} catch (error) {
toast.error("An error occurred. Please try again."); // Error message
}
};
- Install React Toastify to enable toast notifications.
- Configure Toastify in
main.jsx
by adding<ToastContainer />
to the root component. - Add Toast Messages in
Login
andRegister
components to improve user feedback on form submission.
By following these steps, you enhance the user experience by providing immediate feedback on login and registration actions. π
Today, we'll set up an admin endpoint in the backend to fetch all registered user data, excluding passwords. This will be useful for viewing user data in an admin panel.
In the backend/router
folder, create a new file named admin-router.js
to define the routes for the admin functionality.
admin-router.js:
const express = require('express');
const getAllUsers = require('../controllers/admin-controller');
const router = express.Router();
// Route to get all registered users
router.route('/users').get(getAllUsers);
module.exports = router;
In the backend/controllers
folder, create a new file named admin-controller.js
. Here, we'll define a function to fetch all users, excluding sensitive information such as passwords.
admin-controller.js:
const User = require('../models/user-model');
// Fetch all registered users without passwords
const getAllUsers = async (req, res, next) => {
try {
const users = await User.find({}, { password: 0 }); // Exclude passwords
if(!users) {
return res.status(404).json({ message: 'No users found' });
}
res.status(200).json(users);
} catch (error) {
next(error);
}
};
module.exports = getAllUsers;
In index.js
, import the new admin route and use it to enable the /api/admin/users
endpoint.
index.js:
const adminRoute = require('./router/admin-router');
// Admin route for accessing registered users
app.use('/api/admin', adminRoute);
- Open Postman and send a GET request to
http://localhost:3000/api/admin/users
. - You should see a response with a list of registered users, excluding passwords.
- Created a new admin route to manage user data retrieval.
- Configured the controller to fetch user data, excluding passwords.
- Integrated the route into the main app file and tested the endpoint using Postman.
This setup will make it easy to expand our admin functionality for managing users. π