-
Create your project repository on github.
-
clone your repo and cd to it.
-
Run
npm init -y
. -
generate tsconfig.json
npx tsc --init
and uncomment"outDir": "./build"
,"rootDir": "./src"
.
npm i express
npm i -D @types/express rimraf nodemon typescript ts-node
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rimraf ./build && tsc",
"dev": "nodemon"
},
├── src
│ ├── server.ts
basic server.
import express, { Application, Request, Response, NextFunction } from "express";
const app: Application = express();
const PORT = process.env.PORT || 5000;
app.get("/", (req: Request, res: Response) => {
res.status(200).send("Hello World ");
});
app.listen(PORT, () => {
console.log(`server running on port ${PORT}`);
});
- In RootDir add
nodemon.json
.
{
"watch": ["src"],
"ext": ".ts,.js",
"ignore": [],
"exec": ["ts-node ./src/server.ts"]
}
npm run dev
sabir@sabirlinux:~/Desktop/express-typescript-backend$ npm run dev
> express-typescript-backend@1.0.0 dev
> nodemon ./src/server.ts
[nodemon] 3.1.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: ts,json
[nodemon] starting `ts-node ./src/server.ts`
server running on port 5000
Using PostgreSQL in a Docker container makes it easy to set up and manage your database, and it works smoothly across different setups like development and production.
- Go to docker hub and sign-up : DockerHub.
- Login to docker-hub in your terminal by using
docker login
then enter username and pw or using token docker login -u (username)` then enter your token.
To run a PostgreSQL container use the following command:
docker run --name some-postgres -p 5432:5432 -e POSTGRES_PASSWORD=MySecretPassword -d postgres
--name
to name it.-p 5432:5432
Expose container's port 5432 to host's port 5432.-e POSTGRES_PASSWORD=MySecretPassword
Set the environment variablePOSTGRES_PASSWORD
to "MySecretPassword".-d
Run the container in detached mode (-d
flag).postgres
what image we want to pull.
# List all Docker containers.
docker ps -a
# Start a Docker container named <docker container name>.
docker start <docker container name>
# Open a terminal within the Docker container named (express-typescript-postgres-docker).
sudo docker exec -it express-typescript-postgres-docker bash
# Access the PostgreSQL database.
psql -U postgres
# List all databases.
\l
# Connect to a specific database like 'backenddb'.
\c backenddb
- Install pg and dotenv.
npm install pg dotenv
- Install pg types
npm i -D @types/pg
- Add db.ts to /src/config directory.
import { Pool } from "pg";
import dotenv from "dotenv";
dotenv.config();
const pool = new Pool({
user: "postgres",
host: "localhost",
database: "postgres",
password: process.env.DBPASSWORD,
port: 5432,
});
export default pool;
đź“ť You can add a new database with different name other than the default one created with this command in docker terminal
CREATE DATABASE database_name;
.
- Add database schema.
# inside docker terminal
CREATE TABLE users (
user_id INT PRIMARY KEY,
full_name VARCHAR(255),
email VARCHAR(255),
hashed_password VARCHAR(255),
created_at TIMESTAMP,
role VARCHAR(50) DEFAULT 'user'
);
đź“ť You can use PgAdmin4 for easy database management. PgAdmin.
Before we start auth lets secure our app first with cors and helmet.
npm i cors helmet
npm i -D @types/cors
import express, { Application, Request, Response, NextFunction } from "express";
import cors, { CorsOptions } from "cors";
import helmet from "helmet";
import dotenv from "dotenv";
const app: Application = express();
const PORT = process.env.PORT || 5000;
dotenv.config();
const corsOptions: CorsOptions = {
origin: (origin, callback) => {
const origins = String(process.env.CORS_ORIGIN).split(",");
if (!origin || origins.includes(String(origin))) {
callback(null, true);
} else {
callback(new Error("Not allowed."), false);
}
},
credentials: true,
optionsSuccessStatus: 200,
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
};
app.use(cors(corsOptions));
app.use(helmet());
app.use(express.json());
app.get("/", (req: Request, res: Response) => {
res.status(200).send("Hello World ");
});
app.listen(PORT, () => {
console.log(
`🚀 Server ready at ${process.env.SCHEME}://${process.env.HOST}:${process.env.PORT}`
);
});
-
JWT Auth version using prisma
-
file structure :
.
└── src/
├── config/
│ └── db.ts
├── features/
│ └── auth/
│ ├── controllers/
│ │ ├── loginController.ts
│ │ ├── signUpController.ts
│ │ ├── logOutController.ts
│ │ ├── refreshController.ts
│ │ └── meController.ts
│ ├── routes/
│ │ └── index.ts
│ ├── validation/
│ │ └── signUp.ts
│ ├── middlewares/
│ │ └── validate.ts
│ └── utils/
│ └── hashPassword.ts
└── server.ts
- Install auth dependencies.
npm i bcrypt zod cookie cookie-parser jsonwebtoken
npm i -D @types/bcrypt @types/cookie @types/cookie-parser @types/jsonwebtoken
- .env file.
# JWT
JWT_SECRET="" # Run `openssl rand -base64 32` in your CLI to generate a secret
JWT_REFRESH_SECRET="" # Run `openssl rand -base64 32` in your CLI to generate a secret
JWT_EXPIRES_IN=30m
JWT_REFRESH_EXPIRES_IN=30d
# Server
NODE_ENV="development"
HOST="localhost"
SCHEME="http"
PORT="4000"
CORS_ORIGIN="http://localhost:3000,http://localhost:4000"
# Cookie
ACCESS_TOKEN_COOKIE_NAME=ChooseYourCookieName
REFRESH_TOKEN_COOKIE_NAME=ChooseYourCookieName_REFRESH
REFRESH_TOKEN_COOKIE_MAX_AGE=1800000
ACCESS_TOKEN_COOKIE_MAX_AGE=2592000000
- SignUp controller.
// controllers/signupController.ts
import pool from "../../../config/db";
import { hashPassword } from "../../../utils/hashPassword";
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
export const signUpController = async (req: Request, res: Response) => {
const { fullName, email, password } = req.body;
console.log(`name: ${fullName} , email : ${email} and password:${password}`);
const hashedPassword = hashPassword(password);
try {
if (!fullName || !email || !hashedPassword) {
res.status(500).send("data missing try again please !");
return;
} else {
const lowerCaseName = fullName.toLowerCase();
const lowerCaseEmail = email.toLowerCase();
const query = {
text: "INSERT INTO users(full_name, email, hashed_password) VALUES($1, $2, $3) RETURNING user_id",
values: [lowerCaseName, lowerCaseEmail, hashedPassword],
};
const result = await pool.query(query);
const userId = result.rows[0].user_id;
const access_token = jwt.sign({ id: userId }, process.env.JWT_SECRET!, {
expiresIn: process.env.JWT_EXPIRES_IN,
});
const refresh_token = jwt.sign(
{ id: userId },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
);
res.cookie(process.env.REFRESH_TOKEN_COOKIE_NAME!, refresh_token, {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
maxAge: Number(process.env.REFRESH_TOKEN_COOKIE_MAX_AGE),
});
res.cookie(process.env.ACCESS_TOKEN_COOKIE_NAME!, access_token, {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
maxAge: Number(process.env.ACCESS_TOKEN_COOKIE_MAX_AGE),
});
return res.json({ access_token }).status(200);
// res.status(201).send(`User added successfully`);
}
} catch (error) {
res.status(500).json(error);
return;
}
};
export default signUpController;
- Login controller.
// controllers/loginController.ts
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import pool from "../../../config/db";
import { comparePassword } from "../../../utils/hashPassword";
const loginController = async (req: Request, res: Response) => {
const { user_email, password } = req.body;
try {
const query = {
text: "SELECT * FROM users WHERE email = $1",
values: [user_email],
};
const result = await pool.query(query);
const { user_id, email, full_name, role, hashed_password } = result.rows[0];
if (!comparePassword(password, hashed_password)) {
return res.json("error pw");
}
const access_token = jwt.sign({ id: user_id }, process.env.JWT_SECRET!, {
expiresIn: process.env.JWT_EXPIRES_IN,
});
const refresh_token = jwt.sign(
{ id: user_id },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
);
res.cookie(process.env.REFRESH_TOKEN_COOKIE_NAME!, refresh_token, {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
maxAge: Number(process.env.REFRESH_TOKEN_COOKIE_MAX_AGE),
});
res.cookie(process.env.ACCESS_TOKEN_COOKIE_NAME!, access_token, {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
maxAge: Number(process.env.ACCESS_TOKEN_COOKIE_MAX_AGE),
});
return res.json({ access_token }).status(200);
} catch (error) {
throw error;
}
};
export default loginController;
- LogoutController.ts.
// controller/logoutController.ts
import { Request, Response } from "express";
const logoutController = async (_req: Request, res: Response) => {
res
.clearCookie(process.env.REFRESH_TOKEN_COOKIE_NAME!)
.clearCookie(process.env.ACCESS_TOKEN_COOKIE_NAME!)
.status(200)
.end();
};
export default logoutController;
- Me Controller.ts.
// controller/meController.ts
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import pool from "../../../config/db";
const meController = async (req: Request, res: Response) => {
const { SabirAuth } = req.cookies;
if (!SabirAuth) return res.status(401).json({ message: "Unauthorized!" });
const decoded: any = jwt.verify(SabirAuth, process.env.JWT_SECRET!);
const query = {
text: "SELECT * FROM users WHERE user_id = $1",
values: [decoded.id],
};
const result = await pool.query(query);
const { user_id, email, full_name, role, hashed_password } = result.rows[0];
return res
.json({ user_id, email, full_name, role, hashed_password })
.status(200);
};
export default meController;
- Refresh controller.
// controller/meController.ts
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
const refreshController = (req: Request, res: Response) => {
const { SabirAuthRefresh } = req.cookies;
if (!SabirAuthRefresh) {
return res.status(400).json({ message: "Refresh token is expired!" });
}
const decoded: any = jwt.verify(
SabirAuthRefresh,
process.env.JWT_REFRESH_SECRET!
);
const access_token = jwt.sign({ id: decoded.id }, process.env.JWT_SECRET!, {
expiresIn: process.env.JWT_EXPIRES_IN,
});
const refresh_token = jwt.sign(
{ id: decoded.id },
process.env.JWT_REFRESH_SECRET!,
{ expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
);
res.cookie(process.env.REFRESH_TOKEN_COOKIE_NAME!, refresh_token, {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
maxAge: Number(process.env.REFRESH_TOKEN_COOKIE_MAX_AGE),
});
res.cookie(process.env.ACCESS_TOKEN_COOKIE_NAME!, access_token, {
secure: process.env.NODE_ENV === "production",
httpOnly: true,
sameSite: "lax",
maxAge: Number(process.env.ACCESS_TOKEN_COOKIE_MAX_AGE),
});
return res.json({ access_token }).status(200);
};
export default refreshController;
- validation middleware.
// middleware/validate.ts
import { NextFunction, Request, Response } from "express";
import { AnyZodObject } from "zod";
export const validate =
(schema: AnyZodObject) =>
async (req: Request, res: Response, next: NextFunction) => {
try {
await schema.parseAsync({
body: req.body,
query: req.query,
params: req.params,
});
return next();
} catch (error) {
return res.status(400).json(error);
}
};
- Encrypting password.
// utils/hashPassword.ts
import bcrypt from "bcrypt";
const saltRounds = 10;
export const hashPassword = (password: string) => {
const salt = bcrypt.genSaltSync(saltRounds);
return bcrypt.hashSync(password, salt);
};
export const comparePassword = (password: string, hash: string) => {
return bcrypt.compareSync(password, hash);
};
- Zod login validation.
// validation/login.ts
import { z } from "zod";
export const loginSchema = z.object({
body: z.object({
user_email: z
.string({
required_error: "Email is required",
})
.email("Not a valid email"),
password: z.string({
required_error: "Password is required",
}),
}),
});
- Zod signUp validation.
// validation/signUp.ts
import { z } from "zod";
export const signupSchema = z.object({
body: z.object({
fullName: z
.string({
required_error: "Full name is required",
})
.min(3, "Name must be at least 3 characters"),
email: z
.string({
required_error: "Email is required",
})
.email("Not a valid email"),
password: z
.string({
required_error: "Password is required",
})
.min(8, "Password must be at least 8 chars")
.regex(/[A-Z]/, "Password must contain at least one uppercase letter")
.regex(
/[!@#$%^&*(),.?":{}|<>]/,
"Password must contain at least one symbol"
),
}),
});
- Routes.
// routes/index.ts
import express, { Response, Request } from "express";
import { validate } from "../middlewares/validate";
import { loginSchema } from "../validation/login";
import { signupSchema } from "../validation/signUp";
import loginController from "../controllers/loginController";
import signUpController from "../controllers/signupController";
import meController from "../controllers/meController";
import logoutController from "../controllers/logoutController";
import refreshController from "../controllers/refreshController";
loginController;
const authRouter = express.Router();
authRouter.post("/signup", validate(signupSchema), signUpController);
authRouter.post("/login", validate(loginSchema), loginController);
authRouter.get("/me", meController);
authRouter.get("/logout", logoutController);
authRouter.get("/refresh", refreshController);
export { authRouter };
- Application server.
// .server.ts
import express, { Application, Request, Response, NextFunction } from "express";
import cors, { CorsOptions } from "cors";
import helmet from "helmet";
import dotenv from "dotenv";
import { authRouter } from "./features/auth/routes";
import cookieParser from "cookie-parser";
dotenv.config();
const app: Application = express();
const PORT = process.env.PORT || 5000;
const corsOptions: CorsOptions = {
origin: (origin, callback) => {
const origins = String(process.env.CORS_ORIGIN).split(",");
if (!origin || origins.includes(String(origin))) {
callback(null, true);
} else {
callback(new Error("Not allowed."), false);
}
},
credentials: true,
optionsSuccessStatus: 200,
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
};
app.use(cors(corsOptions));
app.use(helmet());
app.use(cookieParser());
app.use(express.json());
app.use("/auth", authRouter);
app.get("/", (req: Request, res: Response) => {
res.status(200).send("Hello Express Backend ! ");
});
app.listen(PORT, () => {
console.log(
`Server running at ${process.env.SCHEME}://${process.env.HOST}:${process.env.PORT}`
);
});