Express js backend starter kit. Backend with expressjs , JWT , typescript,docker and postgresQl.
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" ]
}
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 variable POSTGRES_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
npm install pg dotenv
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;
.
# 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 }`
);
});
.
└── 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
# JWT
JWT_SECRET= "" # Run `openssl rand -base 64 32 ` in your CLI to generate a secret
JWT_REFRESH_SECRET= "" # Run `openssl rand -base 64 32 ` in your CLI to generate a secret
JWT_EXPIRES_IN= 30 m
JWT_REFRESH_EXPIRES_IN= 30 d
# 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
// 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;
// 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;
// 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;
// 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;
// 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;
// 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);
}
};
// 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);
};
// 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" ,
}),
}),
});
// 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/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. get ( "/login" , validate (loginSchema), loginController);
authRouter. get ( "/me" , meController);
authRouter. get ( "/logout" , logoutController);
authRouter. get ( "/refresh" , refreshController);
export { authRouter };
// .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 }`
);
});