writing express projects in 2026
how can we set up a node js project in 2026
we first have to make a folder mkdir new-project
we cd into it
now we isntall the essential typescript dependencies cause its 2026 and nobody uses javascript we use pnpm as well
pnpm add --save-dev typescript @types/node tsx
these are dev dependencies cause they are just here to streamline development out
now lets create a tsconfig file we can do this by
tsc --init
lets go to the .tsconfig file and edit the outdir to be build folder
outDir:"build"
now lets edit the package.json file and add a few scripts that will give us access
to pnpm run dev pnpm run start commands
{
"dependencies": {
"typescript": "^6.0.3",
"@types/node": "^26.0.0",
"tsx": "^4.22.4"
},
"scripts":{
"dev":"tsx watch ./src/index.ts",
"build": "tsc .",
"start": "node ./build/main.js"
}
}
tsx will restart our servers when something changes during dev making it easy to test
now we write a gitignore file to save nodemodules from going to github
node_modules/
.env
we are also add .env here so that we dont accidently push our api keys to git
speaking of api keys lets also add dotenv to our project
pnpm add dotenv
now lets add express
adding express
pnpm add express
pnpm add --save-dev @types/express
lets create now an src folder this is where our app will live
inside src folder we create index.ts
import express from "express"
let app = express();
app.get("/",(req,res)=>{
res.status(200).json({
message:"hello atoms!!"
})
})
app.listen(3000,()=>{
console.log("server started")
})
lets now test if this app works now our server has started in 3000th port
currently we are hardcoding the port , instead lets move the port to the env so we can easily configure it
PORT=3000
import express from "express"
import "dotenv/config"
let app = express();
app.get("/",(req,res)=>{
res.status(200).json({
message:"hello atoms!!"
})
})
app.listen(process.env.PORT,()=>{
console.log("server started")
})
notice that we imported dotenv/config to get access to the process.env loading now lets rerun the server and see if it works
creating routers
if we defined all our routes in one file that will just be difficult to maintain in the future so lets define the routes in a seperate folder using express router
src
|--routes
|-index.ts
|-authRoutes.ts
|-userRoutes.ts
you could call them controllers or whatever i like it routes , and i also like to export the routers from each file then
rexport them in index.ts file so that it can easily be imported by import {authRoutes} from "./routes" rather than individual file names
a fex extra steps for a bit of cleanliness
each routes file will contain a router
import express from "express"
let authRoutes = express.Router();
authRoutes.post("/signup",(req,res)=>{
return res.json({
message:"work in progress"
})
})
authRoutes.post("/signin",(req,res)=>{
return res.json({
message:"work in progress"
})
})
export {authRoutes}
they get imported and exported into the outer folders by routes/index.ts
import {authRoutes} from "./authRoutes"
export {authRoutes}
we import this in main.ts
import express from "express"
import "dotenv/config"
import { authRoutes } from "./routes";
let app = express();
app.use(express.json())
app.use(authRoutes);
app.get("/",(req,res)=>{
...
})
app.listen(process.env.PORT,()=>{
})
and this is how we will structure all of our routes
for my fellow hono users its app.route(); and there is no seperate router just create another app
lets authenticate
before authentication we need schema validation
schema validation is smth we want in almost all routes we can write our own middlewares to do this tasks
lets create a middlewares folder to keep all our middlewares
src
|--routes
|-- middlewares
|--
we are using zod for valdiating our end points lets just call it zodMiddleware or if we feel a bit formal smthn like schemaParserMiddleware whatever floats your boat
import { NextFunction, Request, Response } from "express";
import {z} from "zod"
function zodMiddleware(schema:z.ZodType){
return (req: Request, res: Response, next: NextFunction)=>{
let result = schema.safeParse(req.body);
if(!result.success){
return res.status(400).json({
"success":false,
"errors":result.error.issues.flat()
})
}
req.body = result.data;
next();
}
}
export {zodMiddleware};
this is an example i wrote for express feel free to use this basically its using the factory pattern to create new middlewares when we attach them
before that do run
pnpm add zod
to install zod
preparing auth schemas
lets get our schemas ready i like to store them in a seperate folder named dtos cause at the end of the day they are Data Transfer Objects
import z from "zod"
let signUpDto = z.object({
username: z.string().max(30).min(3),
email:z.email(),
password:z.string().min(8)
})
let signInDto = signUpDto.pick({
email:true,
password:true
})
export type SignUpDto= z.infer<typeof signUpDto>;
export type SignInDto = z.infer<typeof signInDto>;
export {signInDto,signUpDto}
and we can easily use it to any endpoint now
import express from "express"
import {authService} from "../services"
import { signInDto, signUpDto } from "../dto/authDto";
import { zodMiddleware } from "../middlewares";
let authRouter = express.Router();
authRouter.post("/signup",zodMiddleware(signUpDto),(req,res)=>{
authService.signUp(req.body);
})
authRouter.get("/signin",zodMiddleware(signInDto),(req)=>{
authService.signIn(req.body);
})
export {authRouter}
you can see i am jumping the gun and going towards services service layer is where our buisness logic lives to keep the controllers aka routes files organized
we do this all the http related things such as response request parsing etc will be handled here business logic lives in teh service layer where we create services
src
|-- dto
|-- routes
|-- middlewares
|-- services
this is how i like to structure my project
now we get to hard authenication part there are two ways to do auth one is hand rolling it by using jwts bycrypt db this is something you should never do as its stupid and dangerous works for small projects but too much of a headache to perform but u must know it neverthless so just keep them up
jwt middleware
incase you are handlrolling install
pnpm add jsonwebtokens @types/jsonwebtokens
so we get jwt
import { NextFunction, Request, Response } from "express";
import jwt from "jsonwebtoken"
import "dotenv/config"
export default function authMiddleware(req: Request, res: Response, next: NextFunction) {
let authHeader = req.headers?.authorization?.split(" ");
if (!authHeader) {
return res.status(401).json({
"success": false,
"message": "authentication header not present"
})
}
if (authHeader && authHeader[0] != "Bearer") {
return res.status(401).json({
"success": false,
"message": "authenitcation header was not given, make sure Bearer <token> is present"
})
}
let token = authHeader[1];
try {
let payload = jwt.verify(token, process.env.JWT_SECRET!);
req.user = typeof payload != "string" ? payload : undefined;
} catch (e) {
return res.status(401).json({
"success": false,
"message": "authentication header is invalid"
})
}
next();
}
keep this in the auth folder since it modifies the req payload we have to update express’s type
if you were using hono u could have just added it to context
the way u do it, create another folder for cleanliness types and then extend type with express.d.ts
import { JwtPayload } from "jsonwebtoken";
declare global{
namespace Express{
interface Request {
user?: JwtPayload
}
}
}
and there we go its done we have created a jwt middleware but our backend isnt done yet we need a
we still have to set up our auth service
before that we got something big to take care of our database
preparing database
lets start our database by using a docker compose file so its easy to bring it up next time
services:
db:
image: postgres:latest
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
and do docker compose up
docker-compose up -d
add the database connection string to our env we need it later
DATABASE_URL =postgres://<username>:<password>@<host>:<port>/<db-name>
since locally host = localhost or 127.0.0.1
do docker ps to see if your database is running
docker exec -it <container-name> bash to enter bash shell
then psql -U <username> -d <database-name> to enter into psql shell
do \dt see tables \db etc to see things
even create tables if u want but the better approach is always to use schemas and migrations
thats why we will use something like drizzle ORM
preparing ORM
lets first install drizzle orm
pnpm add drizzle-orm pg
well be using postgres driver by pg and we will be using the drizzle-kit to make our migrations easier
pnpm add --save-dev drizzle-kit
now we need to setup drizzle.config.ts
this is typically boiler plate stuff but its good to know
import {defineConfig} from "drizzle-kit"
import "dotenv/config"
export default defineConfig({
dialect:"postgres",
schema:"./src/db/schema.ts",
out:"./migrations",
dbCredentials({
url:process.env.DATABASE_URL!
})
})
now we write our database schema in the db/schema.ts file we specified
import { InferSelectModel } from "drizzle-orm";
import { InferInsertModel } from "drizzle-orm";
import { varchar } from "drizzle-orm/pg-core";
import { integer } from "drizzle-orm/pg-core";
import { serial } from "drizzle-orm/pg-core";
import { pgTable } from "drizzle-orm/pg-core";
export const usersTable = pgTable("user",{
id:serial("id").primaryKey(),
username:varchar("user_name").notNull(),
password: varchar("password").notNull()
})
export type newUser = InferInsertModel<typeof usersTable>
export type User = InferSelectModel<typeof usersTable>
we gotta create a drizzle client which will be used to query our database
so we create db/index.ts
import {Pool} from "pg"
import "dotenv/config"
import {drizzle} from "drizzle-orm/node-postgres"
let pool = new Pool({
connectionString:process.env.DATABASE_URL
})
export let db = drizzle(pool);
now we run pnpm drizzle-kit push to directly push our schema to database
a better practice is to run
pnpm drizzle-kit generate to generate a migration then
apply pnpm drizzle-kit push