Reference : Chai aur Code π
YouTube
Complete Backend Developer course | Part 1
Complete Backend Developer course | Part 2
About
Complete JavaScript Backend Setup is based on the Complete JavaScript Backend Series from the Chai aur Code YouTube Channel.
Special Thanks to Hitesh Sir, for providing us with such amazing content for free! πβ¨
I will suggest to watch the videos for better explanation π
Description
I have tried to write a comprehensive guide to revise Complete Backend Developer course.
Here, I explained things as much as I can.
Note
What is actually a Server? π€
Server is just a software to serve something.
Server can be our phone or laptop.
Server doesn't mean a big computer.
Two major component in backend π§©
A Programming Language
A Database
Use of backend βοΈ
In backend we deals with either
DATA
--> { Array
, String
& Object
}
FILE
--> { Image, Video & PDF }
Third Party API
Requirements βοΈ
Code Editor π»
Visual Studio Code (VS Code) is a free, open-source Integrated Development Environment (IDE) developed by Microsoft, offering features like debugging, version control, and extensions to support various programming languages.
Download VS Code !
A JavaScript run time
Node.js
, Deno
or Bun
Install Node.js
in your machine. Because without node js npm
commands won't work.
Download Node.js !
Confirm node.js
installed or not in your machine
node --version # v20.15.0 or higher
npm --version # 10.7.0 or higher
Create a directory where you are going to set up your project.
For example backend
Drag it or open it using Visual Studio Code (VS Code)
Initialize with npm
npm init
It will ask some questions related to your project. Please answer them accordingly.
Create files in root directory
touch .env .gitignore .prettierrc .prettierignore
Create further directories and files
Run the following commands one by one
mkdir public src
cd src
# Run the commands for creating files and directories in src
touch index.js app.js constants.js
mkdir controllers db models middlewares routes utils
# Back to previous directory
cd ..
cd public
# Run the commands for creating files and directories in public
mkdir temp && touch temp/.gitkeep
Warning
mkdir folder1 folder2 folder3
mkdir temp && touch temp/.gitkeep
Won't work in Windows. π₯
Don't worry!
You can use git-bash
for executing the command on windows.
Or, You can create them manually. π
Install dev dependencies
npm i -D nodemon
npm i -D prettier
Note
Nodemon
used for hot reloading for every single change in files.
Prettier
used to configure the formatting rules.
-D
is a flag to save them as a dev dependency
Install dependencies
npm i express dotenv mongoose
Reference: express | dotenv | mongoose
package.json
"type": "module",
"scripts": {
"dev": "nodemon src/index.js"
},
.gitignore
-> gitignore builder
.prettierrc
{
"singleQuote": false,
"bracketSpacing": true,
"tabWidth": 2,
"trailingComma": "es5",
"semi": true
}
.prettierignore
/.vscode
/node_modules
./dist
*.env
.env
.env.*
.env
PORT=8000
MONGODB_URI=mongodb+srv://<username>:<password>@cluster0.cp5kxxy.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
CORS_ORIGIN=*
Important
Don't include any special character in your password
eg. @
, &
etc
Environment variable must load first in your application
src/index.js
import dotenv from "dotenv"
dotenv.config({
path: "./env"
})
Warning
This is not a valid method to use import
syntax with dotenv
.
Let's config the package.json
to use it as a experimental method.
"scripts": {
"dev": "nodemon -r dotenv/config --experimental-json-modules src/index.js"
},
Important
Resolve errors
By Providing the complete path in import
method, which means with extensions to prevent errors.
import connectDB from "./db/index.js";
//...
import { DB_NAME } from "../constants.js";
//...
src/db/index.js
import mongoose from "mongoose";
const connectDB = () => {}
export default connectDB;
Note
We will connect to database using mongoose
.
Create a method for database connection and store it in a variable so that we can export it as
const connectDB = () => {}
import mongoose from "mongoose";
const connectDB = () => {
try {
} catch (error) {
}
}
export default connectDB;
Note
During database connection some errors can appear.
So it is better to wrap the connection code in the try
and catch
wrapper.
import mongoose from "mongoose";
const connectDB = () => {
try {
} catch (error) {
console.error("MONGODB connect FAILED ", error)
process.exit(0)
}
}
export default connectDB;
Note
In catch part we will console the error with connection failed message.
import mongoose from "mongoose";
import { DB_NAME } from "../constants.js";
const connectDB = async () => {
try {
await mongoose.connect(`${process.env.MONGODB_URI}/${DB_NAME}`)
} catch (error) {
console.error("MONGODB connect FAILED ", error)
process.exit(0)
}
}
export default connectDB;
Important
Remember: "Our database is on another continent."
It will take time to process, must use async
and await
.
src/constants.js
export const DB_NAME = "video"
import mongoose from "mongoose";
import { DB_NAME } from "../constants.js";
const connectDB = async () => {
try {
const connectionInstance = await mongoose.connect(`${process.env.MONGODB_URI}/${DB_NAME}`)
console.log(`\n MongoDB connected !! DB HOST: ${connectionInstance.connection.host}`)
} catch (error) {
console.error("MONGODB connect FAILED ", error)
process.exit(0)
}
}
export default connectDB;
Note
We can store the connection instance in a variable so that we can see some information and console a message for connection success.
src/index.js
import dotenv from "dotenv"
import connectDB from "./db/index.js";
dotenv.config({
path: "./env"
})
connectDB()
Start the server
npm run dev
src/app.js
import express from "express"
const app = express()
export { app }
Note
We will use named export as export { app }
to export the app.
src/index.js
import dotenv from "dotenv"
import connectDB from "./db/index.js";
import { app } from "./app.js";
dotenv.config({
path: "./env"
})
connectDB()
.then()
.catch()
Note
Since, we have created an async
function. So that we can use .then()
and .catch()
methods.
import dotenv from "dotenv"
import connectDB from "./db/index.js";
import { app } from "./app.js";
dotenv.config({
path: "./env"
})
connectDB()
.then(()=>{
})
.catch((err)=>{
console.log("MONGODB Connection failed !!! ", err )
})
Both the method accepts callback
import dotenv from "dotenv"
import connectDB from "./db/index.js";
import { app } from "./app.js";
dotenv.config({
path: "./env"
})
connectDB()
.then(()=>{
app.listen(process.env.PORT || 8000, ()=>{
console.log(`\n Server is running at port ${process.env.PORT}`)
})
})
.catch((err)=>{
console.log("MONGODB Connection failed !!! ", err )
})
middlewares
are used modifying request and response objects, ending the request-response cycle and calling the next middleware function in the stack.
Remember this quotation:
middleware be like "kahin jane se phele mujhse mil kar jana" π
Usually created with use
keyword.
app.use((err, req, res, next) => {
// Some code to be executed
});
Important
middlewares
have 4 parameters
err
The error object that was thrown or passed to the next function with an error argument.
req
The request object.
res
The response object.
next
The next middleware function in the stack.
Install dependencies
npm i cookie-parser cors
Reference: cookie-parser | cors
src/app.js
import express from "express"
import cors from "cors"
import cookieParser from "cookie-parser"
const app = express()
app.use(cors({
origin: process.env.CORS_ORIGIN,
credentials: true
}))
app.use(express.json({ limit: "16kb" }))
app.use(express.urlencoded({ extended: true, limit: "16kb" }))
app.use(express.static("public"))
app.use(cookieParser())
export { app }
A utility is used for handling all requests and responses on every route
src/utils/asyncHandler.js
const asyncHandler = (requestHandler)=> {
return (req, res, next) => {
Promise.resolve(requestHandler(req, res, next)).catch((err) => next(err))
}
}
export { asyncHandler }
Important
It is a higher order function which further return
s a function
We can reuse the utility. π€©
src/utils/ApiError.js
class ApiError extends Error {
constructor(
statusCode,
message = "Something went wrong",
errors = [],
stack = ""
) {
super(message)
this.statusCode = statusCode
this.data = null
this.message = message
this.success = false
this.errors = errors
if (stack) {
this.stack = stack
} else {
Error.captureStackTrace(this, this.constructor)
}
}
}
export { ApiError }
Note
Custom API Error response is very useful for simplifying the custom error messages.
Here we are extending the Error class
available in node.js
src/utils/ApiResponse.js
class ApiResponse {
constructor(statusCode, data, message="success"){
this.statusCode = statusCode,
this.data = data,
this.message = message,
this.success = statusCode < 400
}
}
export { ApiResponse }
HTTP statusCode Responses
Reference: MDN
src/model/user.model.js
import mongoose, { Schema } from "mongoose";
const userSchema = new Schema({});
export const User = mongoose.model("User", userSchema);
Note
user.model.js
helps to identify the file by its filename.
eg. the file is a model file. π€
In models we store data in the form of Schemas
For creating a Schema we use new
keyword, Since Schema is a method which accepts an Object
import mongoose from "mongoose";
new mongoose.Schema({});
Or we can import Schema
object for creating Schema
import mongoose, { Schema } from "mongoose";
new Schema({});
Store it in a variable and export it
import mongoose, { Schema } from "mongoose";
const userSchema = new Schema({});
export User = mongoose.model("User", userSchema);
mongoose.model("User", userSchema)
has two arguments: the collection name and the schema type.
In MongoDB, the collection name becomes lowercase and plural. For example, User becomes users.
We can simply create a data field as given.
const userSchema = new Schema({
username: String
})
But we will follow a best practice.
const userSchema = new Schema({
username: {
type: String,
required: true,
unique: true,
lowercase: true
}
})
Let's explore how to establish a connection or relationship between two data models. π€
// User Data Model
const userSchema = new Schema({
username: {
type: String,
required: true,
unique: true,
lowercase: true,
},
email: {
type: String,
required: true,
unique: true,
}
})
export const User = mongoose.model("User", userSchema)
// CartList Data Model
const productSchema = Schema({
name: {
type: String,
required: true
},
price: {
type: String,
required: true,
default: 0
}
})
const cartModel = Schema({
username: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
},
products: [ productSchema ],
status: {
type: String,
enum: ["PENDING", "CANCELLED", "DELIVERED"].
default: "PENDING"
}
}, { timestamps: true })
export const Cart = mongoose.model("Cart", cartModel)
Important
When we connect two data models, we often use fields like type
and ref
to establish relationships between them
username: {
type: mongoose.Schema.Types.ObjectId,
ref: "User"
},
We want the username from the User
model.
We need to use the type
key to connect and the ref
key
The ref
key holds the value of the collection name of the parent data model.
Since the collection name of the parent is User
, that is why we are referring to it in the child data model.
products: [ productSchema ],
In products, we will use an array
type because a user may purchase one or more products.
status: {
type: String,
enum: ["PENDING", "CANCELLED", "DELIVERED"].
default: "PENDING"
}
The status
field has an enum
key, which provides constant values to be chosen by users.
const cartModel = Schema({ }, { timestamps: true })
In Mongoose, the schema constructor accepts a second object to enable timestamps
Which automatically adds createdAt
and updatedAt
fields to your schema to track document creation and modification times.
npm i mongoose-aggregate-paginate-v2
Reference: mongoose-aggregate-paginate-v2 | mongoDB aggregation pipeline
src/models/video.model.js
import mongoose, { Schema } from "mongoose";
import mongooseAggregatePaginate from "mongoose-aggregate-paginate-v2";
// videoSchema code
// ...
// ...
videoSchema.plugin(mongooseAggregatePaginate)
export const Video = mongoose.model("Video", videoSchema)
Note
videoSchema.plugin(mongooseAggregatePaginate)
helps to write aggregation pipelines
npm i bcrypt jsonwebtoken
src/models/user.model.js
import bcrypt from "bcrypt";
// userSchema code
// ...
// ...
userSchema.pre("save", async function(next){
if(!this.isModified("password")) return next();
this.password = await bcrypt.hash(this.password, 10)
next()
})
userSchema.methods.isPasswordCorrect = async function(password){
return await bcrypt.compare(password, this.password)
}
export const User = mongoose.model("User", userSchema)
Important
Password hashing is a technique to encrypt the given password to a random string.
For hashing we use bcrypt
.
Mongoose provides many hooks like pre
and post
.
By using pre
and post
hooks, you can effectively manage and streamline your data processing workflows in a Mongoose application.
userSchema.pre("event", callbackfn())
pre is a hook
or method
which accepts two values as: Event and a CallBack_fn()
Since, bcrypting is a time consuming task. For safety we will use a async
function in callback.
The callback function acts here as a middleware, So it must have next()
flag to propagate to next node.
Similarly, we can create custom methods using the Schema
userSchema.methods.isPasswordCorrect = async function(){
// Custom method code...
}
Warning
We can't use an arrow function here, because arrow function doesn't have access of this
keyword.
generateAccessToken
userSchema.methods.generateAccessToken = function(){
return jwt.sign(
{
_id: this._id,
email: this.email,
username: this.username,
fullname: this.fullname
},
process.env.ACCESS_TOKEN_SECRET,
{
expiresIn: process.env.ACCESS_TOKEN_EXPIRY
}
)
}
generateRefreshToken
userSchema.methods.generateRefreshToken = function(){
return jwt.sign(
{
_id: this._id,
},
process.env.REFRESH_TOKEN_SECRET,
{
expiresIn: process.env.REFRESH_TOKEN_EXPIRY
}
)
}
Note
jwt.sign({ PAYLOAD }, SECRET, { EXPIRY })
A refresh token has a smaller payload compared to an access token.
Create environment variables for ACCESS_TOKEN_SECRET
, ACCESS_TOKEN_EXPIRY
, REFRESH_TOKEN_SECRET
& REFRESH_TOKEN_EXPIRY
We don't handle files on our local server; instead, we use third-party services like AWS and Cloudinary.
Install dependencies
npm i cloudinary multer
Reference: Cloudinary | Multer
To upload files to Cloudinary, we will use Multer as a middleware.
src/utils/cloudinary.js
import { v2 as cloudinary } from "cloudinary"
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
});
const uploadOnCloudinary = ()=> {
try {
} catch () {
}
}
export { uploadOnCloudinary }
Note
SignUp and create an account on Cloudinary.
Look for cloudinary configuration, It is more safer to store sensitive data on environment file.
Uploading files to Cloudinary can cause errors, so it is better to use a try and catch wrapper around the process.
import { v2 as cloudinary } from "cloudinary"
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
});
const uploadOnCloudinary = ()=> {
try {
} catch (error) {
fs.unlinkSync(localFilePath) // removes the locally saved temporary file as the uplaod operation got failed
return null;
}
}
export { uploadOnCloudinary }
We aim to maintain cleanliness in our application, promptly removing files from temporary storage if an operation fails.
import { v2 as cloudinary } from "cloudinary"
import fs from "fs"
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
});
const uploadOnCloudinary = async (localFilePath)=> {
try {
if (!localFilePath) return null
// Upload the file on cloudinary
const response = await cloudinary.uploader.upload(localFilePath, {
resource_type: "auto"
})
fs.unlinkSync(localFilePath) // removes the locally saved temporary file as the upload operation got success
return response;
} catch (error) {
fs.unlinkSync(localFilePath) // removes the locally saved temporary file as the upload operation got failed
return null;
}
}
export { uploadOnCloudinary }
Note
There is a high chance that the user may not send any files to upload. In such cases, it's best to return a null
value.
Uploading files is a time-consuming task, so it's better to await
the process and make the method asynchronous.
await cloudinary.uploader.upload(localFilePath, {
resource_type: "auto"
})
The upload()
method accepts two arguments as local path of the file and second argument as an object to define resource_type and many other properties related to the file.
We need to return
some response to the user so that we will store it in a variable.
This code snippet is Reusable. π€©
src/middlewares/multer.middleware.js
import multer from "multer";
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, "./public/temp")
},
filename: function (req, file, cb) {
cb(null, file.originalname)
}
})
export const upload = multer({ storage }) // ES6 Module `multer({ storage: storage })`
Important
Multer allows us to choose the type of storage.
Memory Storage
Files are stored in memory as Buffer objects.
This is suitable for handling small files or cases where you want to process the file in-memory without saving it permanently to disk.
Disk Storage
Files are stored on the disk of the server.
You can configure the destination directory and the filename. This is ideal for handling larger files or when you need to persist uploaded files.
For now we will use diskStorage
.
We will see on scroll down. How can we write controller's code effectively.
src/controllers/user.controller.js
import { asyncHandler } from "../utils/asyncHandler.js"
const registerUser = asyncHandler( async (req, res) => {
await res.status(200).json({
message: "OK! Response received"
})
} )
export { registerUser }
src/routes/user.routes.js
import { Router } from "express"
import { registerUser } from "../controllers/user.controller.js"
const router = Router()
router.route("/register").post(registerUser)
export default router
Note
We will use { Router }
from "express"
Update app.js
file
// All imports
// ...
// All middlewares
// ...
// routes
import userRouter from "./routes/user.routes.js"
// routes declaration
app.use("/api/v1/users", userRouter)
export { app }
Important
We will write all routes below the already created middlewares
Because we are going to use the route as a middleware.
POSTMAN Desktop App
Download
Now, We can test the response using Postman
http://localhost:8000/api/v1/users/register
It will give a response as
{
"message": "OK! Response received"
}
On a POST
request at the given URL.
If you want to create a controller which will handle all the user related functions like login or signup.
Think and write the ALGORITHMS for the controller, What you need in your controller and what you are going to do there.
For example:
// ALGORITHM for Register a User
// get user details from frontend
// validation - not empty
// check if user already exists: username, email
// check for images, check for avatar
// upload them to cloudinary, avatar
// create user object - create entry in db
// remove password and refresh token field from response
// check for user creation
// return res
Important
Algorithms are nothing just steps. π
Algorithms help to write code in a structured order.
If we outline all the steps before writing the code,
it saves us a lot of time because we already know what the next step should be.
This eliminates the need to worry about what comes next.
src/routes/user.routes.js
import { Router } from "express"
import { registerUser } from "../controllers/user.controller.js"
import { upload } from "../middlewares/multer.middleware.js"
router.route("/register").post(
upload.fields([
{
name: "avatar",
maxCount: 1
},
{
name: "coverImage",
maxCount: 1
}
]),
registerUser
)
Important
We'll inject a middleware before reaching the route,
router.route("/register").post(upload.fields([])), registerUser)
In this case we are using multer as a middleware for file uploading on cloudinary
We have multiple options for uploading a file like upload.single
upload.any
upload.array
We are using here upload.fields
The upload.fields
method allows you to define multiple file fields in a single request.
This is useful when you need to upload more than one file field, each with its own set of rules.
Each field can have its own name and maximum count of files that can be uploaded.
This provides fine-grained control over the file upload process.
We have the Algorithm to write register user method.
We will start rewriting user.controller.js
file
1. Get data from frontend
import { asyncHandler } from "../utils/asyncHandler.js"
const registerUser = asyncHandler( async (req, res) => {
const { fullName, email, username, password } = req.body;
})
export { registerUser }
Note
Obviously! We don't have any frontend
That is why we are using POSTMAN for sending data
To access data in backend
Use req.body
Also, We can extract the data as
const { fullName, email, username, password } = req.body;
For safety purpose must console the data for the clarity what we are getting.
2. Validation for not empty fields
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
const registerUser = asyncHandler( async (req, res) => {
const { fullName, email, username, password } = req.body;
if (
[fullName, email, username, password].some((field)=> field?.trim() === "")
) {
throw new ApiError(400, "All fields are required")
}
})
export { registerUser }
Note
In the second step, we are checking for empty fields
If any throw an Error
if (
[fullName, email, username, password].some((field)=> field?.trim() === "")
)
The some
method is used to check if any field in the array is empty.
It will return true
.
Sometimes we don't sure, we have the value or not in that situation it is better to use optional chaining.
field?.trim() === ""
?.
is the optional chaining operator in JavaScript.
It allows you to safely access properties or call methods on an object that might be null
or undefined
.
If field is null
or undefined
, the expression field?.trim()
will return undefined
instead of throwing an error.
trim()
removes whitespaces
Since, We have a custom ApiError utility for handling errors
Therefore, Just simply import
it and pass two arguments as the StatusCode and CustomMessage
throw new ApiError(400, "All fields are required")
ApiError
is a class. new
keyword is used to create new class object.
3. Check user exist on not
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
const registerUser = asyncHandler( async (req, res) => {
const { fullName, email, username, password } = req.body;
if (
[fullName, email, username, password].some((field)=> field?.trim() === "")
) {
throw new ApiError(400, "All fields are required")
}
const existedUser = await User.findOne({
$or: [{ username }, { email }]
})
if (existedUser) {
throw new ApiError(409, "User with email or username already exist")
}
})
export { registerUser }
Note
For checking the user already exist or not in the database. We need a database call.
We have user.model.js
file which is talking with database.
So import
it.
Remember our database is on another continent.
It will take time to connect, must use await
const existedUser = await User.findOne({
$or: [{ username }, { email }]
})
User.findOne()
is a method to find a document inside the collection.
To apply filters $or: [{ username }, { email }]
Checks if a user available with the username or email
if (existedUser) {
throw new ApiError(409, "User with email or username already exist")
}
Throws a custom error.
4. Check images: Avatar is uploaded or not
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
const registerUser = asyncHandler( async (req, res) => {
const { fullName, email, username, password } = req.body;
if (
[fullName, email, username, password].some((field)=> field?.trim() === "")
) {
throw new ApiError(400, "All fields are required")
}
const existedUser = await User.findOne({
$or: [{ username }, { email }]
})
if (existedUser) {
throw new ApiError(409, "User with email or username already exist")
}
const avatarLocalPath = req.files?.avatar[0]?.path;
const coverImageLocalPath = req.files?.coverImage[0]?.path;
if (!avatarLocalPath) {
throw new ApiError(400, "Avatar file is required")
}
})
export { registerUser }
Note
Since, We are using multer as middleware.
Therefore, We get files from req.files
as
const avatarLocalPath = req.files?.avatar[0]?.path;
We are assuming that file path is stored in first element.
And for safety purpose, we will try to access it using optional chaining. ?.
Similarly we are getting the local path of cover image.
if (!avatarLocalPath) {
throw new ApiError(400, "Avatar file is required")
}
Lets check for avatar is uploaded or not. If avatar is not uploaded throw an error.
5. Upload images on Cloudinary: Check avatar is uploaded or not
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
const registerUser = asyncHandler( async (req, res) => {
const { fullName, email, username, password } = req.body;
if (
[fullName, email, username, password].some((field)=> field?.trim() === "")
) {
throw new ApiError(400, "All fields are required")
}
const existedUser = await User.findOne({
$or: [{ username }, { email }]
})
if (existedUser) {
throw new ApiError(409, "User with email or username already exist")
}
const avatarLocalPath = req.files?.avatar[0]?.path;
const coverImageLocalPath = req.files?.coverImage[0]?.path;
if (!avatarLocalPath) {
throw new ApiError(400, "Avatar file is required")
}
const avatar = await uploadOnCloudinary(avatarLocalPath)
const coverImage = await uploadOnCloudinary(coverImageLocalPath)
if (!avatar) {
throw new ApiError(400, "Avatar file is required")
}
})
export { registerUser }
Note
For uploading images on Cloudinary we have already created utility file for this.
Uploading images may take time. Must await
the method
const avatar = await uploadOnCloudinary(avatarLocalPath)
And pass the local path of the images to the method.
Again check the avatar is uploaded or not in Cloudinary, If not uploaded throw an error.
6. Create a user object in database
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
const registerUser = asyncHandler( async (req, res) => {
const { fullName, email, username, password } = req.body;
if (
[fullName, email, username, password].some((field)=> field?.trim() === "")
) {
throw new ApiError(400, "All fields are required")
}
const existedUser = await User.findOne({
$or: [{ username }, { email }]
})
if (existedUser) {
throw new ApiError(409, "User with email or username already exist")
}
const avatarLocalPath = req.files?.avatar[0]?.path;
const coverImageLocalPath = req.files?.coverImage[0]?.path;
if (!avatarLocalPath) {
throw new ApiError(400, "Avatar file is required")
}
const avatar = await uploadOnCloudinary(avatarLocalPath)
const coverImage = await uploadOnCloudinary(coverImageLocalPath)
if (!avatar) {
throw new ApiError(400, "Avatar file is required")
}
const user = await User.create({
fullName,
avatar: avatar.url,
coverImage: coverImage?.url || "",
email,
password,
username: username.toLowerCase()
})
})
export { registerUser }
Note
Since, We are dealing with database.
We know that database is in another continent.
Then it will definitely take time to process so use await
.
User.create({})
will create a new object in database.
const user = await User.create({
fullName,
avatar: avatar.url,
coverImage: coverImage?.url || "",
email,
password,
username: username.toLowerCase()
})
We will store the avatar url given by Cloudinary.
Similarly we will store the cover image url but it is not required so chain it optionally.
In case of no input by user show an empty string.
It is better to store username in lowercase.
7. Remove password and Refresh Token fields
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
const registerUser = asyncHandler( async (req, res) => {
const { fullName, email, username, password } = req.body;
if (
[fullName, email, username, password].some((field)=> field?.trim() === "")
) {
throw new ApiError(400, "All fields are required")
}
const existedUser = await User.findOne({
$or: [{ username }, { email }]
})
if (existedUser) {
throw new ApiError(409, "User with email or username already exist")
}
const avatarLocalPath = req.files?.avatar[0]?.path;
const coverImageLocalPath = req.files?.coverImage[0]?.path;
if (!avatarLocalPath) {
throw new ApiError(400, "Avatar file is required")
}
const avatar = await uploadOnCloudinary(avatarLocalPath)
const coverImage = await uploadOnCloudinary(coverImageLocalPath)
if (!avatar) {
throw new ApiError(400, "Avatar file is required")
}
const user = await User.create({
fullName,
avatar: avatar.url,
coverImage: coverImage?.url || "",
email,
password,
username: username.toLowerCase()
})
const createdUser = await User.findById(user._id).select(
"-password -refreshToken"
)
if (!createdUser) {
throw new ApiError(500, "Something went wrong while registering the user")
}
})
export { registerUser }
Note
For removing password and refresh token.
We can store created user reference in a variable using findById()
method.
User.findById(user._id)
will look for the match using id.
const createdUser = await User.findById(user._id).select(
"-password -refreshToken"
)
By chain it to select the object, we can remove password and refreshToken from the response object.
select()
method uses a weird syntax I mean a string π€
Pass the name of fleids to remove from the response object using (-) sign as prefix. eg. -password
For more safety check again registered user is available or not?
If not available throw error response with status code 500 because this is server response error.
8. Return response
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const registerUser = asyncHandler( async (req, res) => {
const { fullName, email, username, password } = req.body;
if (
[fullName, email, username, password].some((field)=> field?.trim() === "")
) {
throw new ApiError(400, "All fields are required")
}
const existedUser = await User.findOne({
$or: [{ username }, { email }]
})
if (existedUser) {
throw new ApiError(409, "User with email or username already exist")
}
const avatarLocalPath = req.files?.avatar[0]?.path;
const coverImageLocalPath = req.files?.coverImage[0]?.path;
if (!avatarLocalPath) {
throw new ApiError(400, "Avatar file is required")
}
const avatar = await uploadOnCloudinary(avatarLocalPath)
const coverImage = await uploadOnCloudinary(coverImageLocalPath)
if (!avatar) {
throw new ApiError(400, "Avatar file is required")
}
const user = await User.create({
fullName,
avatar: avatar.url,
coverImage: coverImage?.url || "",
email,
password,
username: username.toLowerCase()
})
const createdUser = await User.findById(user._id).select(
"-password -refreshToken"
)
if (!createdUser) {
throw new ApiError(500, "Something went wrong while registering the user")
}
return res.status(201).json(
new ApiResponse(200, createdUser, "User registered successfully")
)
} )
export { registerUser }
Important
Keep in mind
"It is not necessary that your code will work in one go.
Bugs, Errors are the part of programming. π"
By: Hitesh Sir! π
As we have already discussed that we don't have any frontend yet.
That is why we will send data through POSTMAN
STEP 1: Open postman and create a new tab by clicking +
sign
Enter the url as http://localhost:8000/api/v1/users/register
STEP 2: Select POST
method from dropdown
GET
POST
PUT
HEAD
STEP 3: We will send data via Body
Body has multiple ways to send data to backend as
We will choose the form-data, it has key-value
pairs to send data.
Important
It has advantage over raw
and x-www-form-urlencoded
.
Form data is able to send files.
Since, We are sending files. So form-data is the better option. π
In keys write the names all the fields we are accepting in backend.
As fullName
username
email
password
avatar
coverImage
.
For better explanation, How to use postman?
Check out the videos. π€
When the user does not pass a cover image.
As I mentioned earlier, programming often involves solving bugs and errors. π€
When we try to create a user without a cover image, we get an error.
This is not a backend or server error; it's a core JavaScript error.
Here is the solution
src/routes/user.routes.js
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const registerUser = asyncHandler( async (req, res) => {
const { fullName, email, username, password } = req.body;
if (
[fullName, email, username, password].some((field)=> field?.trim() === "")
) {
throw new ApiError(400, "All fields are required")
}
const existedUser = await User.findOne({
$or: [{ username }, { email }]
})
if (existedUser) {
throw new ApiError(409, "User with email or username already exist")
}
const avatarLocalPath = req.files?.avatar[0]?.path;
// const coverImageLocalPath = req.files?.coverImage[0]?.path;
let coverImageLocalPath;
if (req.files && Array.isArray(req.files.coverImage) && req.files.coverImage.length > 0) {
coverImageLocalPath = req.files.coverImage[0].path
}
if (!avatarLocalPath) {
throw new ApiError(400, "Avatar file is required")
}
const avatar = await uploadOnCloudinary(avatarLocalPath)
const coverImage = await uploadOnCloudinary(coverImageLocalPath)
if (!avatar) {
throw new ApiError(400, "Avatar file is required")
}
const user = await User.create({
fullName,
avatar: avatar.url,
coverImage: coverImage?.url || "",
email,
password,
username: username.toLowerCase()
})
const createdUser = await User.findById(user._id).select(
"-password -refreshToken"
)
if (!createdUser) {
throw new ApiError(500, "Something went wrong while registering the user")
}
return res.status(201).json(
new ApiResponse(200, createdUser, "User registered successfully")
)
} )
export { registerUser }
Important
Since we are not checking for the cover image, if the user does not provide a cover image, we need to create a checkpoint.
let coverImageLocalPath;
if (req.files && Array.isArray(req.files.coverImage) && req.files.coverImage.length > 0) {
coverImageLocalPath = req.files.coverImage[0].path
}
Here, we will try to access the cover image from the files if available.
Otherwise, the value will be undefined
.
We will receive an object.
asset_id
public_id
version
version_id
signature
width: 720
heigth: 720
resource_type: "image"
created_at
tags
bytes
type: "upload"
etag
placeholder: false
url
secure_url
asset_folder
display_name
original_filename
api_key
We have written method for register a user.
Now, We will write another method for login by following these Algorithm
// ALGORITHM for User Login Method
// req body -> data
// check for username or email
// find the user
// password check
// access and refresh token
// send cookie
1. Create Login method in user.controller.js
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
} )
export {
registerUser,
loginUser
}
Create a login method using
asyncHandler()
andexport
it as named export
2. Get data from request body
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const { username, email, password } = req.body
} )
export {
registerUser,
loginUser
}
Note
Assuming that we will get username
, email
and password
from the user.
3. Check for username or email
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const {username, email, password} = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
} )
export {
registerUser,
loginUser
}
Important
Since, We can login with either username
or email
.
Therefore, We need atleast one of them to proceed login.
4. Find the user if exist else throw an error
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const {username, email, password} = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
} )
export {
registerUser,
loginUser
}
Note
User.findOne()
is a method to find a document in database.
And we have learned that- "Our database is in another continent".
So await
it, because database call takes time. π
We can use filter methods in findOne()
inside {}
and the method starts with $
sign
const user = await User.findOne({
$or: [{username}, {email}]
})
This $or
method will find the user basis of username
or email
.
If the user not exist throw an error.
5. Password validation
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const { username, email, password } = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
} )
export {
registerUser,
loginUser
}
Note
In the user model, We have a method for password comparison named as isPasswordCorrect()
.
Don't get confused between user
and User
π€
User
is a schema model acts as a bridge between the database and our local server. It helps to talk with database.
The user
is a variable instance holds the data of found user after the execution of findOne()
method, and we will use it for validations.
Again user.isPasswordCorrect()
is a method for password validation. Which helps to match the entered password with the password saved in the database.
Don't forget to await
it.
6. Generate access token and refresh token
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const generateAccessAndRefreshTokens = (userId) => {
}
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const { username, email, password } = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
generateAccessAndRefreshTokens(user._id)
} )
export {
registerUser,
loginUser
}
For generating access and refresh token, We will create a separate internal method which will accept user id generated by mongodb.
generateAccessAndRefreshTokens(user._id)
here_id
is generated by mongodb.It is better to write internal methods after the
import
s.
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const generateAccessAndRefreshTokens = (userId) => {
try {
} catch (error) {
throw new ApiError(500, "Something went wrong while generating refresh and access token")
}
}
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const { username, email, password } = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
generateAccessAndRefreshTokens(user._id)
} )
export {
registerUser,
loginUser
}
Since, We are dealing with database errors may occur.
For the safety purpose wrap the code inside atry
andcatch
block.
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const generateAccessAndRefreshTokens = async (userId) => {
try {
const user = await User.findById(userId)
} catch (error) {
throw new ApiError(500, "Something went wrong while generating refresh and access token")
}
}
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const { username, email, password } = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
generateAccessAndRefreshTokens(user._id)
} )
export {
registerUser,
loginUser
}
Note
For generating tokens we will have to find a user.
We will find the user with the passed userId
in the parameter.
User.findById(userId)
deals with database, It will definitely take time to process, must await
it. and the method should be async
method.
Store the received user in a variable.
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const generateAccessAndRefreshTokens = async (userId) => {
try {
const user = await User.findById(userId)
const accessToken = user.generateAccessToken()
const refreshToken = user.generateRefreshToken()
user.refreshToken = refreshToken
await user.save({ validateBeforeSave: false })
return { accessToken, refreshToken }
} catch (error) {
throw new ApiError(500, "Something went wrong while generating refresh and access token")
}
}
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const { username, email, password } = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
generateAccessAndRefreshTokens(user._id)
} )
export {
registerUser,
loginUser
}
Important
We have methods for generating access and refresh token in User model
const accessToken = user.generateAccessToken()
const refreshToken = user.generateRefreshToken()
generateAccessToken()
and generateRefreshToken()
are the methods.
Now we will save the refresh token in database so that we don't have to login for every task.
Database document is nothing just an object so we can access it's attributes.
user.refreshToken = refreshToken
user.refreshToken = refreshToken
await user.save()
user.save()
method is used to save the refresh token in the database.
Since, password is required for every input in database.
So we can simply add a parameter as
await user.save({validateBeforeSave: false})
Once, access and refresh tokens are generated we can return
them to the user.
This time we will return
tokens as an object.
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const generateAccessAndRefreshTokens = async (userId) => {
try {
const user = await User.findById(userId)
const accessToken = user.generateAccessToken()
const refreshToken = user.generateRefreshToken()
user.refreshToken = refreshToken
await user.save({validateBeforeSave: false})
return { accessToken, refreshToken }
} catch (error) {
throw new ApiError(500, "Something went wrong while generating refresh and access token")
}
}
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const { username, email, password } = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
const { accessToken, refreshToken } = await generateAccessAndRefreshTokens(user._id)
} )
export {
registerUser,
loginUser
}
Note
Inside the generateAccessAndRefreshTokens()
method we are dealing with database. It is better to await
the method.
The method provides us access and refresh tokens in the form of an object. So we can destruct values from the object as { accessToken, refreshToken }
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const generateAccessAndRefreshTokens = async (userId) => {
try {
const user = await User.findById(userId)
const accessToken = user.generateAccessToken()
const refreshToken = user.generateRefreshToken()
user.refreshToken = refreshToken
await user.save({validateBeforeSave: false})
return { accessToken, refreshToken }
} catch (error) {
throw new ApiError(500, "Something went wrong while generating refresh and access token")
}
}
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const {username, email, password} = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
const { accessToken, refreshToken } = await generateAccessAndRefreshTokens(user._id)
const loggedInUser = await User.findById(user._id).select("-password -refreshToken")
} )
export {
registerUser,
loginUser
}
Important
The user
variable doesn't have the value of refresh token because, we have accessed the user before generating tokens.
const user = await User.findOne({
$or: [{username}, {email}]
})
That is why, we need the updated user instance as well as we have some unnecessary data fields like password and refresh token in the created user
instance.
So time to make another database call to get updated user
instance excluding password and refresh token, and store it in another variable as loggedInUser
const loggedInUser = await User.findById(user._id).select("-password -refreshToken")
7. Sending cookie
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const generateAccessAndRefreshTokens = async (userId) => {
try {
const user = await User.findById(userId)
const accessToken = user.generateAccessToken()
const refreshToken = user.generateRefreshToken()
user.refreshToken = refreshToken
await user.save({validateBeforeSave: false})
return { accessToken, refreshToken }
} catch (error) {
throw new ApiError(500, "Something went wrong while generating refresh and access token")
}
}
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const { username, email, password } = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
const { accessToken, refreshToken } = await generateAccessAndRefreshTokens(user._id)
const loggedInUser = await User.findById(user._id).select("-password -refreshToken")
const options = {
httpOnly: true,
secure: true
}
} )
export {
registerUser,
loginUser
}
Note
When we send cookies, we have to design some options for the cookies.
Options are nothing just an Object
const options = {
httpOnly: true,
secure: true
}
These options defines that cookies are modifiable in the server only. But we can access and see them in frontend.
Return a response
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
const generateAccessAndRefreshTokens = async (userId) => {
try {
const user = await User.findById(userId)
const accessToken = user.generateAccessToken()
const refreshToken = user.generateRefreshToken()
user.refreshToken = refreshToken
await user.save({validateBeforeSave: false})
return { accessToken, refreshToken }
} catch (error) {
throw new ApiError(500, "Something went wrong while generating refresh and access token")
}
}
// Register User
// ...
// ...
const loginUser = asyncHandler( async (req, res) => {
const { username, email, password } = req.body
if (!username || !email) {
throw new ApiError(400, "username or email is required")
}
const user = await User.findOne({
$or: [{username}, {email}]
})
if (!user) {
throw new ApiError(404, "User does not exist")
}
const isPasswordValid = await user.isPasswordCorrect(password)
if (!isPasswordValid) {
throw new ApiError(401, "Invalid user credentials")
}
const { accessToken, refreshToken } = await generateAccessAndRefreshTokens(user._id)
const loggedInUser = await User.findById(user._id).select("-password -refreshToken")
const options = {
httpOnly: true,
secure: true
}
return res
.status(200)
.cookie("accessToken", accessToken, options)
.cookie("refreshToken", refreshToken, options)
.json(
new ApiResponse(
200,
{
user: loggedInUser, accessToken, refreshToken
},
"User logged in Successfully"
)
)
} )
export {
registerUser,
loginUser
}
Note
In the response object we wil return status
code, cookie
s and a json
object.
return res.status(200)
We have access of cookie
s because we have injected a middleeware for cookie
s in app.js
.
return res
.status(200)
.cookie("accessToken", accessToken, options)
cookie(name, value, options)
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
// generateAccessAndRefreshTokens method
// ...
// ...
// Register User
// ...
// ...
// Login User
// ...
// ...
const logoutUser = asyncHandler( async (req, res) => {
} )
export {
registerUser,
loginUser,
logoutUser
}
Create a logout user method using
asyncHandler()
.
Important
For logout we don't need any kind of password validation.
Logout must be a secured method so that nobody could logout any other user by providing their username or email.
Authentication middleware is a custom middleware. π
src/middlewares/auth.middleware.js
import { asyncHandler } from "../utils/asyncHandler";
export const verifyJWT = asyncHandler( async (req, res, next) => {
})
Create a method as
verifyJWT
with the help ofasyncHandler()
We have learned that middleware has four parameters as
(err, req, res, next) => {}
import { asyncHandler } from "../utils/asyncHandler";
import { ApiError } from "../utils/ApiError";
export const verifyJWT = asyncHandler( async (req, res, next) => {
try {
} catch (error) {
throw new ApiError(401, "Invalid Access Token")
}
})
Start a
try
andcatch
block.
import { asyncHandler } from "../utils/asyncHandler";
import { ApiError } from "../utils/ApiError";
export const verifyJWT = asyncHandler( async (req, res, next) => {
try {
const token = req.cookies?.accessToken || req.header("Authorization")?.replace("Bearer ", "")
if (!token) {
throw new ApiError(401, "Unauthorized request")
}
} catch (error) {
throw new ApiError(401, "Invalid Access Token")
}
})
Important
We have already saved some information in cookie
s during the user login. So now cookie
s are accessible.
We can get values from cookie
s if available.
We are trying to get access token from cookie
s using optional chaining (?.)
for safety.
req.cookies?.accessToken
If the above method will get fail then get the value from header.
req.header("Authorization")?.replace("Bearer ", "")
If token not available throw an error.
import { asyncHandler } from "../utils/asyncHandler";
import { ApiError } from "../utils/ApiError";
import jwt from "jsonwebtoken";
export const verifyJWT = asyncHandler( async (req, res, next) => {
try {
const token = req.cookies?.accessToken || req.header("Authorization")?.replace("Bearer ", "")
if (!token) {
throw new ApiError(401, "Unauthorized request")
}
const decodedUser = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)
} catch (error) {
throw new ApiError(401, "Invalid Access Token")
}
})
We will use
jwt
for verifying and theverify()
method has two arguments token and secret.
import { asyncHandler } from "../utils/asyncHandler";
import { ApiError } from "../utils/ApiError";
import jwt from "jsonwebtoken";
import { User } from "../models/user.model";
export const verifyJWT = asyncHandler( async (req, res, next) => {
try {
const token = req.cookies?.accessToken || req.header("Authorization")?.replace("Bearer ", "")
if (!token) {
throw new ApiError(401, "Unauthorized request")
}
const decodedUser = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)
const user = await User.findById(decodedUser?._id).select("-password -refreshToken")
if (!user) {
throw new ApiError(401, "Invalid Access Token")
}
} catch (error) {
throw new ApiError(401, "Invalid Access Token")
}
})
Note
Store the decoded user in a new variable without password and refresh token.
For finding the decoded user in the database we have only one bridge, I mean User
model schema.
If the user not exist throw an error.
import { asyncHandler } from "../utils/asyncHandler";
import { ApiError } from "../utils/ApiError";
import jwt from "jsonwebtoken";
import { User } from "../models/user.model";
export const verifyJWT = asyncHandler( async (req, _, next) => {
try {
const token = req.cookies?.accessToken || req.header("Authorization")?.replace("Bearer ", "")
if (!token) {
throw new ApiError(401, "Unauthorized request")
}
const decodedUser = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET)
const user = await User.findById(decodedUser?._id).select("-password -refreshToken")
if (!user) {
throw new ApiError(401, "Invalid Access Token")
}
req.user = user;
next()
} catch (error) {
throw new ApiError(401, "Invalid Access Token")
}
})
Important
As discussed earlier, middlewares have the capacity of modifying req
and res
object.
In this case we are adding an object in req
object.
And propagate it to the next node in the list using next()
flag.
Since, we are doing nothing with the res
object. So that we can replace it by a underscore (_)
in the parameter.
1. Create login route
import { Router } from "express"
import { loginUser, registerUser } from "../controllers/user.controller.js"
import { upload } from "../middlewares/multer.middleware.js"
const router = Router()
// User registration route
// ...
router.route("/login").post(loginUser)
export default router
2. Create logout route
import { Router } from "express"
import { loginUser, logoutUser, registerUser } from "../controllers/user.controller.js"
import { upload } from "../middlewares/multer.middleware.js"
import { verifyJWT } from "../middlewares/auth.middleware.js"
const router = Router()
// User registration route
// ...
router.route("/login").post(loginUser)
// Secured routes
router.route("/logout").post(verifyJWT, logoutUser)
export default router
Note
We have a middleware as auth.middleware.js
for verifying.
Inject it in the logout route before hitting the logout user controller.
Middleware executes one after one in the series, and they must have a next()
flag for the propagation.
post(middleware1, middleware2, endpoint)
Back to the user.controller.js
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
// generateAccessAndRefreshTokens method
// ...
// ...
// Register User
// ...
// ...
// Login User
// ...
// ...
const logoutUser = asyncHandler( async (req, res) => {
await User.findByIdAndUpdate(
req.user._id,
{
$set: {
refreshToken: undefined
}
},
{
new: true
}
)
} )
export {
registerUser,
loginUser,
logoutUser
}
Important
To logout we need a user.
In the logout method we don't send any data from the frontend like username
or email
.
Because user is already logged in that is why we are trying to log out.\
Also, we have injected a middleware in the logout route as verifyJWT
.
Which modifies the req
object and adds a new attribute as user
with user data fields in the form of object.
Accessing values from the user
object embedded with req
object is possible with dot notation as req.body.[ data-field ]
When we logout a user, we must have to reset or clear the value of refresh token.
User.findByIdAndUpdate()
is a mongodb method used to find and update any data field in the document. It accepts three arguments as
findByIdAndUpdate(
req.user._id, // finds the user on the basis of id generated by mongodb
{
$set: {
refreshToken: undefined // Value to be updated
}
},
{
new: true // for getting the most updated value
}
)
import { asyncHandler } from "../utils/asyncHandler.js"
import { ApiError } from "../utils/ApiError.js"
import { User } from "../models/user.model.js"
import { uploadOnCloudinary } from "../utils/cloudinary.js"
import { ApiResponse } from "../utils/ApiResponse.js"
// generateAccessAndRefreshTokens method
// ...
// ...
// Register User
// ...
// ...
// Login User
// ...
// ...
const logoutUser = asyncHandler( async (req, res) => {
await User.findByIdAndUpdate(
req.user._id,
{
$set: {
refreshToken: undefined
}
},
{
new: true
}
)
const options = {
httpOnly: true,
secure: true
}
return res
.status(200)
.clearCookie("accessToken", options)
.clearCookie("refreshToken", options)
.json(
new ApiResponse(200, {}, "User logged Out")
)
} )
export {
registerUser,
loginUser,
logoutUser
}
Set options for
cookie
s and remove savedcookie
s usingclearCookie()
.