🚀 Your go-to guide for learning Express.js – the most popular Node.js framework for building web applications and APIs.
This repository documents my Express.js learning journey, with organized notes, code examples, project setups via a basic RESTfull API project , demonstrating the Contents section.
Whether you're new to backend development or expanding your Node.js skills, this guide can help you build solid Express apps with confidence.
- ⚙️ Setting Up an Express Project
- 🧱 Folder Structure (JS & TypeScript)
- Hello World Application
- 🔄 Middleware – Built-in, Custom, and 3rd-party
- 🗺️ Routing (Basic to Advanced)
- 📦 RESTful API Design with Express
- 🧩 Handling Request and Response
- ❗ Error Handling Strategies
- 🛡️ Authentication & Authorization (JWT, sessions)
- 🧪 Validation with
express-validatororjoi - 🌍 Environment Configuration (
dotenv) - 🧰 Connecting with Databases (MongoDB)
- 🧼 Clean Code & Project Structure Tips
- 🐳 Dockerizing Express Apps (Optional)
- Use the
npm initcommand to create apackage.jsonfile for your application. For more information on howpackage.jsonworks, see Specifics of npm’s package.json handling. - This command prompts you for a number of things, such as the name and version of your application. For now, you can simply hit RETURN to accept the defaults for most of them, with the following exception:
entry point: (index.js)
Enter app.js, or whatever you want the name of the main file to be. If you want it to be index.js, hit RETURN to accept the suggested default file name.
Now, install Express in the myapp directory and save it in the dependencies list. For example:
$ npm install express
To install Express temporarily and not add it to the dependencies list:
$ npm install express --no-save
- for javascript
my-express-app/
│
├── node_modules/
├── public/ # Static files (images, CSS, JS)
├── routes/ # Route handlers
│ └── userRoutes.js
├── controllers/ # Logic behind routes
│ └── userController.js
├── models/ # Mongoose/Sequelize models
│ └── userModel.js
├── middlewares/ # Custom middleware
│ └── auth.js
├── utils/ # Utility functions
│ └── logger.js
├── config/ # Configuration files (DB, env, etc.)
│ └── db.js
├── app.js # Express app setup
├── server.js # Server entry point
├── .env # Environment variables
├── package.json
└── README.md- for typescript
my-express-ts-app/
│
├── src/
│ ├── controllers/
│ │ └── user.controller.ts
│ ├── routes/
│ │ └── user.routes.ts
│ ├── models/
│ │ └── user.model.ts
│ ├── middlewares/
│ │ └── auth.middleware.ts
│ ├── config/
│ │ └── db.config.ts
│ ├── utils/
│ │ └── logger.ts
│ ├── types/
│ │ └── custom.d.ts # Custom type declarations
│ ├── app.ts # Express app setup
│ └── server.ts # Entry point
│
├── dist/ # Compiled JS files (after build)
├── .env
├── .gitignore
├── tsconfig.json
├── package.json
└── README.mdimport {express} from "express"
const app = express()
const port = 8080
app.get("/",(req,res)=>{
res.send("<h1>Hello wolrd</h1>")
})
app.listen(port,()=>{
console.log(`Listening on Port ${port}`)
})-
a middleware i somthing set between the request and response
-
basically used for some tasks like verifyingJWT , verifying roles , error handlers , loggers and so on
-
logger example:
import { log } from 'console';
import { format } from 'date-fns';
import { v4 as uuid } from 'uuid';
import { promises as fsPromises , existsSync} from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export const logEvents = async (message , logname) =>{
const object = `${new Date()}\t${uuid()}\t${message}\n`;
const logDir = path.join(__dirname,'..','logs');
const logFilePath = path.join(logDir,logname);
try{
if(!existsSync(logDir)){
await fsPromises.mkdir(logDir);
}
await fsPromises.appendFile(logFilePath,object)
}catch(err){
log(err);
}
}
export const logger = (req,res,next)=>{
logEvents(`${req.method}\t${req.headers.origin}\t${req.url}`,"reqLogs.txt")
log(`${req.method} ${req.path} :: ${new Date()}`);
next();
}- Authentication = who are you
- Authorization = who does what
- an open standard for securely transmitting information between parties as a JSON object. It's a compact, self-contained way to represent claims (statements about an entity, usually a user) for authentication and authorization
How JWTs work:
-
Authentication: A user authenticates themselves (e.g., by providing credentials).
-
JWT Issuance: A REST API issues a JWT token to the user, containing information about their identity and permissions (Access Token and Refresh Token)
-
Token Storage: The user stores the JWT securely . (Only Store ==Access Tokens== in Memory , do not store in local storage or cookie , ==Refresh Token== sent as httpOnly cookie , not accessable via javascript , must have expiry at some point)
-
Request Inclusion: The user includes the JWT in subsequent requests to the server.
-
Token Verification: The server verifies the JWT's signature and claims to ensure its authenticity and authorize the user's actions.
-
==Access Token== : Short Time Token (5-15 mintues)
-
==Refresh Token==: Long Time Token (day to days)
-
Hazards:
- XSS(Cross-Site Scripting)
- CSRF(CS Request Forgery)
-
Access Token Process in a nutshell:
- Access Token is issued at Authorization
- Client uses the Access Token for protected routers (api access) until expires
- every time a client make a request to a protected route a middleware verfies the Access Token
- when the Access Token expires , clients should send their Refresh tokens to refresh endpoint to get a new Access Token.
-
Refresh Token Process in a nutshell:
-
Refresh Token is issued at Authorization.
-
When the Access Token expires, the client sends the Refresh Token to a specific refresh endpoint to get a new Access Token — without asking the user to log in again.
-
That the Refresh Token is valid
-
That it hasn’t expired, been revoked, or tampered with
This is often done by looking it up in a database or verifying its signature if it’s a JWT. -
must be allowed to expire or logout
-
example in loginController.js
-
import {readFile , writeFile} from 'fs/promises'; import { fileURLToPath } from 'url'; import path from 'path'; import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { config } from 'dotenv';
const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const dataPath = path.join(__dirname,'..','model','users.json');
let data = JSON.parse( await readFile(dataPath) );
config();
export const handleLogin = async (req,res)=>{ const {user,pass} = req.body; if(!user || !pass)
return res.status(400).json({"message":"Username and Passowrd Are required !"});
const user_ = data.find(person => person.username === user);
if(!user_)
return res.sendStatus(401) // Unauthorized.
const match = await bcrypt.compare(pass,user_.password);
if(!match)
return res.sendStatus(401) // Unauthorized.
else{
const accessToken = jwt.sign(
{"username":user_.username},
process.env.ACCESS_TOKEN_SECRET,
{expiresIn: '30s'}
);
const RefreshToken = jwt.sign(
{"username":user_.username},
process.env.REFRESH_TOKEN_SECRET,
{expiresIn: '1d'}
);
// Saving Refresh Toke with Current User
const otherUsers = data.filter(person => person.username !== user_.username);
const currentUser = {...user_,RefreshToken};
data = [...otherUsers,currentUser];
await writeFile(dataPath,JSON.stringify(data));
res.cookie('jwt',RefreshToken,{
httpOnly:true,
sameSite: 'None',
secure:true,
maxAge: 24*60*60*1000 // one day in millsec
})
res.json({accessToken});
}
}
- jwt.sin() takes 3 parameters :
- #### 1. **Payload** (`{"username": user_.username}`)
- The data you want to include in the token. .. (Encoded Data)
- This is typically user information (but never sensitive like passwords).
- It gets encoded into the token.
#### 2. **Secret Key** (`process.env.ACCESS_TOKEN_SECRET`)
- A secret string used to sign the token.
- On the server side, you'll need this same secret to **verify** the token later using `jwt.verify()`.
- It's important to keep this secret safe (usually stored in `.env`).
#### 3. **Options** (`{ expiresIn: '30s' }`)
- Optional settings for the token.
- `expiresIn` sets the token expiration time (e.g., `'30s'`, `'1h'`, `'2d'`).
---
- an example on refreshing the refreshToken:
```js
import {readFile} from 'fs/promises';
import { fileURLToPath } from 'url';
import path from 'path';
import jwt from 'jsonwebtoken';
import { config } from 'dotenv';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const dataPath = path.join(__dirname,'..','model','users.json');
let data = JSON.parse(
await readFile(dataPath)
);
config();
export const handleRefreshToken = (req,res)=>{
const cookie = req.cookie;
if(!cookie?.jwt)
return res.sendStatus(401);
console.log(cookie.jwt);
const refreToken = cookie.jwt;
const user_ = data.find(person => person.refreToken === refreToken);
if(!user_)
return res.sendStatus(403) // forbidden.
jwt.verify(
refreToken,
process.env.REFRESH_TOKEN_SECRET,
(err,decoded)=>{
if(err || user_.username !== decoded.username)
return res.sendStatus(403);
const accessToken = jwt.sign(
{"username":decoded.username},
process.env.ACCESS_TOKEN_SECRET,
{expiresIn:'30s'}
);
res.json({accessToken})
}
);
}
if (!user_)
return res.sendStatus(403); // Forbidden This means: The refresh token exists in the client's cookie. But there's no matching user in your users.json who owns that token. → So, the server refuses to give a new access token.
if (err || user_.username !== decoded.username)
return res.sendStatus(403);
This means:This means: Either the token is invalid or expired (err) Or the token's username was tampered with or doesn’t match the user. → So again, access is forbidden.
- verifyJWT middleware:
import jwt from 'jsonwebtoken';
import env from 'dotenv'
export const verifyJWT = (req,res,next) => {
const authHeader = req.headers['authorization'];
if(!authHeader)
return res.sendStatus(401); // Unauhorized
console.log(authHeader); // Bearer token
const token = authHeader.split(' ')[1];
jwt.verify(
token,
process.env.ACCESS_TOKEN_SECRET,
(err,decoded)=>{
if(err) return res.sendStatus(403); // forbidden (invalid token)
req.user = decoded.username;
next();
}
);
}- we use it like this (for ex):
app.get('/dashboard', verifyJWT, (req, res) => {
res.json({ message: `Hello, ${req.user}` });
});Q: why the refresh token should be stored with users ? , does it nessassary to do so ? A: Yes, it is necessary to store the refresh token with the user (typically in the database), and here's why:
-
The access token is short-lived (e.g., 30s), to minimize damage if it's stolen.
-
The refresh token is longer-lived (e.g., 1 day) and is used to get a new access token when the current one expires without requiring the user to log in again.
-
To validate that the refresh token is legitimate
You can verify the token with the secret, but that’s not enough. If you don’t store it, you won’t know whether the token was revoked or not. -
To enable logout / revocation
-
If the user logs out, you should invalidate the refresh token.
-
This is only possible if you're tracking which tokens are active, which requires storage.
-
-
To prevent reuse after compromise
-
If someone steals a refresh token, you need a way to check:
-
Is this token one of the valid ones?
-
Has it been used or rotated already?
-
-
-
User logs in → server creates access and refresh tokens.
-
Server:
-
Sends access token to client (in memory or cookie).
-
Sends refresh token (usually HTTP-only cookie).
-
Stores refresh token in the DB, mapped to the user.
-
-
When access token expires, client sends refresh token to
/refresh. -
Server:
-
Verifies token signature and checks if it's in the DB.
-
If valid → issues new access token.
-
If not in DB → rejects the request.
-
-
Anyone who steals a refresh token can use it forever until it expires.
-
You can't revoke it if the user logs out or the token is compromised.
-
No way to implement refresh token rotation, which is a good security practice.