This is a backend built with Node.js, Express, TypeScript, and Prisma ORM. It follows modern best practices for API development, including strict type safety, structured error handling, security measures, and environment validation.
Designed to be modular and maintainable, the project features a clean architecture, making it easy to extend with new functionalities.
🛠️ Core Features
✅ TypeScript – Fully typed backend for maintainability
✅ Express.js – Lightweight and fast web framework
✅ Prisma ORM – Type-safe database interactions
✅ PostgreSQL – Relational database
🎯 Development & Code Quality
✅ Feature-Based Structure – Each feature has its own folder, keeping everything related to a feature (routes, schemas, types, services, controllers, repositories) together for better maintainability and scalability
✅ ESLint + Prettier – Code linting, formatting and autoformat on save
✅ Zod Validation – Strict schema validation for request & environment variables
✅ VSCode debugger
🔐 Environment & Security
✅ Environment Validation – Ensures required .env variables exist
✅ Helmet & Security Headers – Protects against web vulnerabilities
✅ Rate Limiter, Host whitelisting middleware
⚡ API & Middleware
✅ Request Validation – Uses Zod for body, params, and query validation
✅ Error Handling Middleware – Centralized error handling with PostgreSQL error handling (Ref)
✅ Unified Response Structure – Uses uni-response for consistent API responses
🧪 Testing & CI/CD
✅ Vitest – Unit and integration testing
✅ Husky + Lint-Staged – Enforces pre-commit linting and testing
🛑 Server Management
✅ Graceful Shutdown – Ensures proper cleanup of database & open connections during shutdown (Ref)
This project follows a feature-based modular structure, where each feature (e.g., user) has its own isolated folder containing everything related to that feature.
📂 Project Structure:
src/
│── config/ # Configuration (e.g., environment variables, Prisma, security)
│── constants/ # Shared constants (messages, enums, etc.)
│── features/ # Feature-based modular structure
│ ├── user/ # User feature module
│ │ ├── __tests__/ # Unit tests (vitest)
│ │ ├── controllers/ # Handles HTTP requests (Express-dependent)
│ │ ├── repositories/ # Database interactions (Prisma-dependent)
│ │ ├── routes/ # Express API routes (Express-dependent)
│ │ ├── schemas/ # Zod validation schemas (Framework-agnostic)
│ │ ├── services/ # Business logic (Completely framework-independent)
│ │ ├── types/ # TypeScript interfaces & types
│── middleware/ # Global Express middlewares
│── utils/ # Helper functions
│── app.ts # Express app setup
│── server.ts # Entry point
Each feature is self-contained, meaning everything related to "users" is inside features/user/
🎯 Benefit:
💡 You can easily add or remove features without affecting other parts of the app.
🔹 No Cluttering, Even as the Project Grows Large – The feature-based structure ensures that related files stay together, preventing scattered code.
🔹 Everything in One Place – Developers can find all logic related to a feature (controllers, services, repositories, schemas) in a single folder, reducing confusion.
🔹 No Ambiguity in Large Systems – Since each feature is self-contained, developers always know which controller, service, or repository to use, making onboarding and scaling easier.
🔹 Scalability & Maintainability – Adding a new feature means simply creating a new folder under features/, without modifying unrelated parts of the app.
✅ Handles HTTP requests and responses
✅ Calls the service layer for business logic
✅ Only responsible for Express-specific logic
📄 Example: user.controller.ts
import { Request, Response } from "express";
import { UserService } from "../services/user.service";
export class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
async getUsers(req: Request, res: Response) {
const users = await this.userService.getAllUsers();
res.json({ success: true, data: users });
}
}
Express-specific logic stays here (e.g., req, res)
Business logic is in the service layer (so it’s framework-agnostic)
🎯 Benefit:
💡 Can switch from Express to Fastify/NestJS by just changing the controllers.
✅ Contains core business logic
✅ Does NOT depend on Express or Prisma
✅ Interacts with repositories for data retrieval\
📄 Example: user.service.ts
import { UserRepository } from "../repositories/user.repository";
export class UserService {
private userRepository: UserRepository;
constructor() {
this.userRepository = new UserRepository();
}
async getAllUsers() {
return await this.userRepository.getUsers();
}
}
- No dependency on Express or HTTP requests
- Calls repository for database access
🎯 Benefit:
💡 Can be reused in a CLI app, background worker, or GraphQL API without changes.
✅ Handles all database queries
✅ Uses Prisma (or any ORM, easily replaceable)
✅ Interacts only with services/, never controllers
📄 Example: user.repository.ts
import { prisma } from "@/config/prisma.config";
export class UserRepository {
async getUsers() {
return await prisma.user.findMany();
}
}
- Keeps database logic separate from business logic
- Easy to swap Prisma for another ORM (e.g., Drizzle, TypeORM)
🎯 Benefit:
💡 Can change the database or ORM without affecting services/controllers.
✅ Defines API endpoints
✅ Maps controllers to Express routes
📄 Example: user.routes.ts
import { Router } from "express";
import { UserController } from "../controllers/user.controller";
const router = Router();
const userController = new UserController();
router.get("/", (req, res) => userController.getUsers(req, res));
export default router;
- Controllers are injected into routes for better testability
- Only Express-dependent part is here
🎯 Benefit:
💡 Can switch to NestJS, Fastify, or Hono by only changing routes & controllers.
✅ Uses Zod for request validation ✅ Completely framework-independent
📄 Example: user.schema.ts
import { z } from "zod";
export const createUserSchema = z.object({
email: z.string().email(),
name: z.string().optional(),
});
- Schemas don’t depend on Express, so they can be used anywhere
- Validation logic is reusable (can be used in GraphQL, CLI, or workers)
🎯 Benefit:
💡 Easier to enforce validation rules across different application layers.
Layer | Purpose | Benefit |
---|---|---|
Controllers | Handle HTTP requests | Framework-dependent, easily replaceable |
Services | Business logic | Framework-agnostic, reusable anywhere |
Repositories | Database interactions | Can switch ORM (Prisma, TypeORM, Drizzle) |
Routes | Maps controllers to APIs | Only responsible for Express routing |
Schemas | Data validation | Reusable validation logic across app |
mkdir express-ts-prisma && cd express-ts-prisma
npm init -y
npm install --save-dev typescript tsx nodemon @types/node tsc-alias
Create
tsconfig.json
npm install express cors dotenv
npm install --save-dev @types/express @types/cors
Create
.env.dev
File from.env.example
Create src/config/env-config.ts // Env Configuration file Create src/config/env-schema.ts // Schema for environment variables
npm install --save-dev eslint prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-prettier eslint-plugin-node eslint-plugin-import eslint-plugin-simple-import-sort eslint-plugin-unicorn eslint-plugin-security eslint-config-prettier
Create
eslint.config.js
Create
.prettierrc.json
Create
.prettierignore
📌 Prettier will ignore these files & folders (same format as .gitignore
).
create
.vscode/settings.json
to Autoformat using Prettier on save
Create Database and Shadow Database
Update
.env.dev
File
DATABASE_URL="postgresql://dev_user:dev_password@localhost:5432/dev_db"
SHADOW_DATABASE_URL="postgresql://dev_user:dev_password@localhost:5432/dev_db_shadow"
npm install @prisma/client
npm install --save-dev prisma
npx prisma init
model User {
id String @id @default(uuid())
email String @unique
name String?
createdAt DateTime @default(now())
}
npx prisma generate
npx prisma migrate dev --name init
npm install --save-dev husky lint-staged
npx husky install
npm set-script prepare "husky install"
npx husky add .husky/pre-commit "npx lint-staged"
Modify package.json
// Runs linters (ESLint, Prettier) only on changed files before committing.
"lint-staged": {
"**/*.{ts,json,md}": ["eslint --fix", "prettier --write"]
}
Add Pre-Push Hook
// Before git push trigger tests & build validation.
npx husky add .husky/pre-push "npm run lint && npm run format && npm run test && npm run build"
"scripts": {
"prebuild": "npm run lint && npm run format && npm run test",
"build": "tsc",
"start": "node dist/server.js",
"dev": "nodemon --ext ts --exec tsx src/server.ts",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext ts --fix",
"format": "prettier --write .",
"test": "vitest",
"prepare": "npx husky install"
}
# Start Dev Server
npm run dev
# Lint Code
npm run lint
npm run lint:fix
# Format Code
npm run format
npm install --save-dev vitest @vitest/coverage-v8 @types/jest supertest @types/supertest
Create test files at src\features\user\__tests__
npm i helmet express-rate-limit
npm install pino pino-pretty pino-http
npm install -D @types/pino @types/pino-pretty @types/pino-http
Create src\middleware\pino-logger.ts
If you liked it then please show your love by ⭐ the repo