Project Architecture
Project Architecture
Directory Structure
src/
├── config/
│ ├── env.ts # Environment variables
│ ├── database.ts # Database configuration
│ └── index.ts # Config barrel
├── types/
│ ├── index.ts # Shared types
│ ├── api.ts # API request/response types
│ └── models.ts # Domain model types
├── middleware/
│ ├── auth.ts # Authentication middleware
│ ├── validation.ts # Request validation
│ └── error-handler.ts
├── services/
│ ├── user-service.ts
│ ├── email-service.ts
│ └── index.ts
├── repositories/
│ ├── user-repository.ts
│ └── base-repository.ts
├── routes/
│ ├── user-routes.ts
│ └── index.ts
├── utils/
│ ├── logger.ts
│ └── helpers.ts
└── index.ts # Entry point
Layered Architecture
Routes (HTTP Layer)
↓
Controllers (Request/Response)
↓
Services (Business Logic)
↓
Repositories (Data Access)
↓
Database
Type-First Design
// types/models.ts - define types first
export interface User {
id: string;
name: string;
email: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
export type UserRole = 'admin' | 'user' | 'guest';
export interface CreateUserDTO {
name: string;
email: string;
role?: UserRole;
}
export interface UpdateUserDTO {
name?: string;
email?: string;
role?: UserRole;
}
Build Configuration
Build Configuration
TypeScript Config
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
package.json
{
"name": "my-ts-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"typecheck": "tsc --noEmit",
"prepare": "husky"
},
"dependencies": {
"express": "^4.18.0",
"zod": "^3.22.0",
"dotenv": "^16.3.0"
},
"devDependencies": {
"typescript": "^5.4.0",
"tsx": "^4.7.0",
"vitest": "^1.2.0",
"@types/node": "^20.11.0",
"@types/express": "^4.17.0",
"eslint": "^8.56.0",
"prettier": "^3.2.0",
"husky": "^9.0.0"
}
}
API Contract Types
API Contract Types
Request/Response Types
// types/api.ts
export interface ApiResponse<T> {
data: T;
meta?: {
page: number;
limit: number;
total: number;
};
}
export interface ApiError {
error: string;
code: string;
details?: Record<string, string[]>;
}
export interface PaginationQuery {
page?: string;
limit?: string;
sort?: string;
order?: 'asc' | 'desc';
}
export interface SearchQuery extends PaginationQuery {
q?: string;
}
Route Handler Types
// types/routes.ts
import { Request, Response, NextFunction } from 'express';
export type Handler<
Params = {},
ResBody = unknown,
ReqBody = unknown,
ReqQuery = {}
> = (
req: Request<Params, ResBody, ReqBody, ReqQuery>,
res: Response<ResBody>,
next: NextFunction
) => Promise<void> | void;
// Usage
type GetUserHandler = Handler<
{ id: string }, // Params
ApiResponse<User>, // ResBody
{}, // ReqBody
{} // ReqQuery
>;
Coding Conventions
Coding Conventions
Naming Conventions
// Interfaces - PascalCase, no prefix
interface User { }
interface CreateUserDTO { }
// Type aliases - PascalCase
type UserRole = 'admin' | 'user';
type Result<T> = Success<T> | Failure;
// Enums - PascalCase
direction enum Status {
Active = 'active',
Inactive = 'inactive'
}
// Functions - camelCase, verb first
function createUser() { }
function fetchUsers() { }
// Constants - camelCase or UPPER_SNAKE_CASE
const maxRetries = 3;
const API_TIMEOUT = 5000;
// Files - kebab-case
// user-service.ts
// auth-middleware.ts
Type Patterns
// Use interfaces for public APIs
interface UserServiceInterface {
findById(id: string): Promise<User | null>;
create(dto: CreateUserDTO): Promise<User>;
}
// Use type aliases for unions and computed types
type RequestHandler = (req: Request, res: Response) => Promise<void>;
// Prefer readonly for immutable data
type ReadonlyUser = Readonly<User>;
// Use branded types for IDs
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };