In this tutorial you will learn how login and signup works behind the scene as we will develop a production ready login and registration form using MERN Stack. We will be using mongoDb as our database and Express.js as our server. We will use “jsonwebtoken” package to generate a access token and “bcryptjs” package to hash password before stroing it into the database. The main objective of this tutorial is to make you understand how does json web token generation works and hashing works and also why are they used.
Node.js and npm installed
MongoDb initialised for storing user's data
Here is a small asmr tutorial on how to build login and registration page. From here you will be able to build the UI of the form and the only extra thing you will have to work on is to add the api connection part.
Create a repository for the project and inside the repository create another two different folders - client for the frontend and server for the backend.
mkdir login_register_mern
cd login_register_mern
mkdir client server
On the server folder initialise node.js app and typescript
npm init -y
npm i typescript
npx tsc -init
install package and dev-dependencies
npm i express mongoose dotenv cors bcryptjs jasonwebtoken
npm i -D @types/express @types/bcryptjs @types/jsonwebtoken ts-node-dev
update package.json and tsconfig.json files
// in package.json file
"style" : {
"build" : "tsc",
"start" : "node dist/server.js",
"dev" : "ts-node-dev --transipile-only --respawn src/server.ts"
}
// in tsconfig.json file
"rootDir" : "./src",
"outDir" : "./dist"
Create .env file on the roote directory ( server ) to store sensetive and important urls.
touch .env
Store all the sensetive urls on .env file
PORT = 5001
DB_URL = ''
JWT_SECRET_KEY = "DoNotShare"
JWT_EXPIRY_DATE = '30d'
Inside server folder create a src folder and inside it create some essential folders along with a server file.
mkdir src && cd src
touch server.ts
mkdir config controller model router utils
Initialise app, connect the routes and database. ( src/server.ts )
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import authRouter from './router/authRouter';
import { dbConfig } from './config/dbconfig';
// import ting the root url's from .env file
const PORT = porcess.env.PORT ?? 8000;
const uri = porcess.env.DB_URI ?? '';
// initialising app
const app = express();
// connecting the server with database
dbConfig(uri);
// using middlewares
app.use(cors({
origin: '*',
}));
app.use(express.urlencoded());
app.use(express.json());
// using the routes
app.use('/api/auth', authRouter);
app.listen(port, () => console.log('server started on port: 5001'));
connect your server with the database. ( src/config/dbConfig.ts )
import mongoose from 'mongoose';
// function that connects server with the database.
export const dbConfig = (uri: string) => {
mongoose.connect(uri)
.then(() => console.log('server connected to database successfuly'))
.catch(err => console.log(err));
};
Create a user model. ( model/user.mode.ts )
import { Schema, model } from 'mongoose';
const UserSchema = new Schema({
full_name: {
type: String,
required: [true, 'please enter your full name']
},
email: {
type: String,
required: [true, 'please enter your email']
},
password: {
type: String,
required: [true, 'please enter your password']
},
}, { timestamps: true });
const User = model('users', UserSchema);
export default User;
Create helping components like asyncHandler, errorHandler, token generator and password hasher.
cd utils
touch asyncHandler.ts errorHandler.ts jwt.utils.ts bcryptjs.utils.ts
Inside asyncHandler.ts file create async handler function
import { Request, Response, NextFunction, RequestHandler } from 'express';
type Handler = (req: Request, res: Response, next: NextFunciton) => Promise<any>;
// async handler function
export const asyncHandler = (fun: Handler) : RequestHandler => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fun(req,res,next)).catch(err => next(err));
};
};
Create custom error handler file inside the file errorHandler.ts
class errorHandler extends Error {
statusCode : number;
status: 'success' | 'fail' | 'error';
success: boolean
constructor (message: string, statusCode: number) {
super(message)
this.statusCode = statusCode
this.status = statusCode >= 400 && statusCode < 500? 'fail' : 'error'
this.success = false
Error.captureStackTrace(this, errorHandler);
};
};
export default errorHandler;
Create a token generator and verifiyer on the jwt.utils.ts file
import jwt from 'jsonwebtoken';
const jwtSecretKey = process.env.JWT_SECRET_KEY ?? 'Shhh'; // This is the secret key which is used for token generation and decoding of token
const jwtExpiryDate = process.env.JWT_EXPIRY_DATE ?? '30d'; // This is the expiry date of the token, when the token exceds this time the generated token won't work so that for new token user have to login again.
// token generator function
export const generateToken = (data: IPayload) => {
const token = jwt.sign(data, jwtSecretKey, { expiresIn: jwtExpiryDate as any });
return token;
};
// verify token
export const verifyToken = (token: string) => {
return jwt.verify(token, jwtSecretKey) as jwt.JwtPayload;
};
“jsonwebtoken” is one of the package that is used to generate as it’s name says ‘JSON web token’. This token comes in hand when ever you login to a site, close the site and visit it again after few hours. The generated token when you first loged in will automatically logs in for you such that the second visit to site is direct to your dashaboard.
create a password hasher and verifyier in bcryptjs.utils.ts file
import bcryptjs from 'bcryptjs';
// hash password
export const hashPassword = async (password: string) => {
const Salt = await bcryptjs.genSalt(12); // you can generate salt of any number but usually number between 10 - 15 are prefered.
const hashedPassword = bcryptjs.hash(password, Salt);
return hashedPassword; // returns a hashed password
};
// verify password
export const verifyPassword = ( password: string, hash: string ) => {
return bcryptjs.compare(password, hash); // returns a boolean value
};
“bcryptjs” is another package which is basically used to hash any kind of sensitive datas ( password, contact info and many more ) of user before storing into a database.
create a file auth.controller.ts in the controller folder
import { Request, Response } from 'express';
import { asyncHandler } from '../utils/asyncHandler';
import errorHandler from '../utils/errorHandler';
import User from '../model/UserSchema';
import { hashPassword, verifyPassword } from '../utils/bcryptjs.utils';
import { generateToken } from '../utils/jwt.utils';
// register new user
export const newRegister = asyncHandler( async (req: Request, res: Response) => {
const {password, ...data} = req.body; // we receive the datas from body of request
if(!data) { // returns error if all the required datas are not filled
throw new errorHandler('please enter all the required details', 406);
};
const hashedPassword = await hashPassword(password); // the function returns hashed password
if(!hashedPassowrd) { // If any problem occures while returning hashed password throws an error
throw new errorHandler('something went wrong, please try again', 500);
};
const user = await User.create({ password: hashedPassword, ...data}); // creats new user data on the database storing all the user input data and hashed password insted of the actual password
res.status(200).json({ // returns a success response to the client
message: 'user successfuly registered',
data: user,
status: 'success',
success: true,
});
};
// login user
export const loginUser = asyncHandler( async (req: Request, res: Response) => {
const { email, password } = req.body; // receiving email and password from the body of request
if(!email){ // throws an error if email is not typed
throw new errorHandler('please enter your email', 406);
};
if(!password) { // throws an error if password is not typed
throw new errorHandler('please enter your password', 406);
};
const user = await User.findOne({email}); // finds user from the database using typed email
if(!user) {
throw new errorHandler('user does not exists', 404);
};
const verifiedPassword = verifyPassword(password, user.password); // checks for the password and returns boolean value
if(!verifiedPassword) { // if the password is false, returns an error
throw new errorHandler('either password or email is incorrect', 406);
};
const accessToken = generateToken({ // this function generats JSON web token for easy access
full_name: user.full_name,
email: user.email,
password: user.password
});
if(! accessToken) { // if any problem comes while token generation throws an error
throw new errorHandler('something went wrong please try again, 500);
};
res.status(200).json({ // sends success response along with the access token
message: 'user successfuly loged in',
data: user,
status: 'success',
success: true,
accessTokne,
});
};
Create auth.router.ts file to rout your controllers.
import { Router } from 'express';
import { newRegister, loginUser } from '../controller/auth.controllers';
const authRouter = Router();
authRouter.post('/register', newRegister);
authRouter.post('/login', loginUser);
export default authRouter;
Go to client folder and initilise new next app
cd client && npx create-next-app@latest .
// After this it will ask you questions answer them as per your requirenent if writing the code then just press enter and continue
install essential packages
npm i react-hot-toast react-icons @tanstack/query react-hook-form @hookform/resolver yup axios
Create a folder named components inside app folder and again inside component folder create 3 folders (schema, interface and cards)
cd app && mkdir components
cd components && mkdir cards schema interface
create two files inside cards ( LoginCard.tsx & RegisterCard.tsx)
cd cards && touch LoginCard.tsx RegisterCard.tsx
"use client";
import Link from "next/link";
import React from "react";
import { useForm } from "react-hook-form";
import { CiMail } from "react-icons/ci";
import { CiLock, CiUnlock } from "react-icons/ci";
import { ILogin } from "../interface/form.interface";
import { yupResolver } from "@hookform/resolvers/yup";
import { loginSchema } from "../schema/form.schema";
import { useMutation } from "@tanstack/react-query";
import toast from "react-hot-toast";
import { LoginUser } from "@/app/api/form.api";
const LoginCard = () => {
const { mutate } = useMutation({
mutationFn: LoginUser,
mutationKey: ["newUser"],
onSuccess: (data) => {
toast.success(data.message);
reset();
},
onError: (err) => {
toast.error(err.message);
reset();
},
});
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm({
resolver: yupResolver(loginSchema),
});
const sendData = (data: ILogin) => {
mutate(data);
};
return (
<div className="max-w-100 w-full rounded-md shadow-lg/30 bg-white p-4 flex flex-col items-center justify-center gap-8">
<h1 className="w-full font-bold text-xl text-center text-black/70">
Login
</h1>
<form
onSubmit={handleSubmit(sendData)}
className="w-full flex flex-col items-start justify-center gap-4"
>
<div className="w-full px-2 relative">
<CiMail className="absolute right-4 top-2 font-bold text-xl text-black" />
<input
type="text"
placeholder="e-mail"
{...register("email")}
className="w-full font-md text-md pl-3 pr-8 py-1 rounded-sm outline-none border border-zinc-500/32 text-black"
/>
{errors && errors.email ? (
<p className="w-full text-thin text-end text-red-500 text-sm">
{errors.email.message}
</p>
) : (
""
)}
</div>
<div className="w-full px-2 relative">
<CiUnlock className="absolute right-4 top-2 font-bold text-xl text-black cursor-pointer" />
<input
type="password"
placeholder="password"
{...register("password")}
className="w-full font-md text-md pl-3 pr-8 py-1 rounded-sm outline-none border border-zinc-500/32 text-black"
/>
{errors && errors.password ? (
<p className="w-full text-thin text-end text-red-500 text-sm">
{errors.password.message}
</p>
) : (
""
)}
</div>
<div className="w-full px-2 mt-2">
<button
type="submit"
className="w-full flex items-center justify-center rounded-sm font-medium text-md hover:bg-blue-500 hover:text-white ease duration-300 border border-blue bg-white text-blue-500 py-2"
>
Login
</button>
</div>
</form>
<hr className="w-full h-[1px] bg-zinc-800" />
<p className="w-full text-center font-medium text-sm text-black">
Don't have an account?{" "}
<Link
className="font-medium text-sm text-blue-500"
href={"/auth/register"}
>
Sign up
</Link>
</p>
</div>
);
};
export default LoginCard;
"use client";
import React from "react";
import { yupResolver } from "@hookform/resolvers/yup";
import { useForm } from "react-hook-form";
import { registerSchema } from "../schema/form.schema";
import { IRegister } from "../interface/form.interface";
import { CiMail, CiUnlock } from "react-icons/ci";
import Link from "next/link";
import { useMutation } from "@tanstack/react-query";
import { RegisterUser } from "@/app/api/form.api";
import toast from "react-hot-toast";
const RegisterCard = () => {
const { mutate } = useMutation({
mutationFn: RegisterUser,
mutationKey: ["NewUser"],
onSuccess: (data) => {
toast.success(data.message);
reset();
},
onError: (err) => {
toast.error(err.message);
reset();
},
});
const {
reset,
register,
handleSubmit,
formState: { errors },
} = useForm({
resolver: yupResolver(registerSchema),
});
const sendData = (data: IRegister) => {
mutate(data);
};
return (
<div className="max-w-100 w-full rounded-md shadow-lg/30 bg-white p-4 flex flex-col items-center justify-center gap-8">
<h1 className="w-full font-bold text-xl text-center text-black/70">
Register
</h1>
<form
onSubmit={handleSubmit(sendData)}
className="w-full flex flex-col items-start justify-center gap-4"
>
<div className="w-full px-2 relative">
<CiMail className="absolute right-4 top-2 font-bold text-xl text-black" />
<input
type="text"
placeholder="e-mail"
{...register("email")}
className="w-full font-md text-md pl-3 pr-8 py-1 rounded-sm outline-none border border-zinc-500/32 text-black"
/>
{errors && errors.email ? (
<p className="w-full text-thin text-end text-red-500 text-sm">
{errors.email.message}
</p>
) : (
""
)}
</div>
<div className="w-full px-2 relative">
<CiUnlock className="absolute right-4 top-2 font-bold text-xl text-black cursor-pointer" />
<input
type="password"
placeholder="password"
{...register("password")}
className="w-full font-md text-md pl-3 pr-8 py-1 rounded-sm outline-none border border-zinc-500/32 text-black"
/>
{errors && errors.password ? (
<p className="w-full text-thin text-end text-red-500 text-sm">
{errors.password.message}
</p>
) : (
""
)}
</div>
<div className="w-full px-2 relative">
<CiUnlock className="absolute right-4 top-2 font-bold text-xl text-black cursor-pointer" />
<input
type="password"
placeholder="confirm password"
{...register("c_password")}
className="w-full font-md text-md pl-3 pr-8 py-1 rounded-sm outline-none border border-zinc-500/32 text-black"
/>
{errors && errors.c_password ? (
<p className="w-full text-thin text-end text-red-500 text-sm">
{errors.c_password.message}
</p>
) : (
""
)}
</div>
<div className="w-full px-2 mt-2">
<button
type="submit"
className="w-full flex items-center justify-center cursor-pointer rounded-sm font-medium text-md hover:bg-blue-500 hover:text-white ease duration-300 border border-blue bg-white text-blue-500 py-2"
>
Sign up
</button>
</div>
</form>
<hr className="w-full h-[1px] bg-zinc-800" />
<p className="w-full text-center font-medium text-sm text-black">
Already have an account?{" "}
<Link
className="font-medium text-sm text-blue-500"
href={"/auth/login"}
>
Sign in
</Link>
</p>
</div>
);
};
export default RegisterCard;
Insied schema folder create a file named form.schema.ts, here we will define the schema of login and register form.
imoprt * as yup from 'yup';
// Login schema
export const loginSchema = yup.object({
email: yup.string().email('please enter a valid email').required('please enter your email'),
password: yup.string().required('please enter your password'),
});
// register new user
export const registerSchema = yup.object({
full_name: yup.string().required('please enter your full name'),
email: yup.string().emai('please enter a valid email').required('please enter your email'),
password: yup.string().required('please enter the password').min(8, 'your password must be atleast of 8 characters').matches(/d+$/, 'your password must contain atleast one digit'),
c_password: yup.string().required('please re-enter the password').oneof([yup.ref('password')], 'password did not matched'),
});
Inside the interface folder create a file form.interface.ts where we will define the interface of the login and registration form.
// login form interface
export interface ILogin {
email: string,
password: string,
};
// registration form interface
export interface IRegister {
full_name: string,
email: string,
password: string,
};
Finally after creating all of those components, go to app folder then create a folder named auth and again inside the auth folder create two folder login and register.
cd app && mkdir auth
cd auth && mkdir login register
Again create a file named page.tsx in both of the file.
../login/page.tsx
import React from 'react';
import LoginCard from '@/components/cards/LoginCard';
const page = () => {
return (
<div className="w-full h-screen flex items-cetner justify-center">
<LoginCard/>
</div>
);
};
../register/page.tsx
import React from 'react';
import RegisterCard from '@/components/cards/RegisterCard';
import page from '../../page';
const page = () => {
return (
<div calssName="w-full h-screen flex items-center justify-center">
<RegisterCard/>
</div>
);
};
Lastly create an api folder inside app folder and create a file named form.api.ts
cd ../
mkdir api
cd api && touch form.api.ts
open form.api.ts
Create axios instance and connect frontend with backend.
import axios from 'axios';
import { ILogin, IResigster } from '@/components/interface/form.interface';
// axios instance
const axiosInstance = axios.create({
baseURL = 'http://localhost:port/api/auth'
});
// login form connection
export const LoginUser = async (data: ILogin) => {
try{
const response = await axiosInstance.post('/login', data);
return response.message;
} catch (err: any) {
return err.message;
};
};
// register form connection
export const RegisterUser = async (data: IRegister) => {
try{
const response = await axiosInstance.post('/register', data);
return response.message;
} catch (err: any) {
return err.message;
};
};
With this you have successfully completed building production ready login/registration form and you also might have understood how all the social medias and other applications keep record of your accounts. Thank you for giving your valuable time to read my blog. After this if still you have some doubts regarding this project, your have certain part that you are still confused about then please feel free to contact me or mail me i’ll be explaining it clearly to you guys.
click here to check the demo!