Skip to content

Module_architecture

Chapter 21: Application Architecture Patterns

Section titled “Chapter 21: Application Architecture Patterns”

Building Scalable TypeScript Applications with TypeORM

Section titled “Building Scalable TypeScript Applications with TypeORM”

21.1 Understanding Application Architecture

Section titled “21.1 Understanding Application Architecture”

A well-structured TypeScript application follows clean architecture principles. Whether you’re using Express, Fastify, or any other framework, the core patterns remain the same.

TypeScript Application Architecture
================================================================================
+-------------------+ +-------------------+ +-------------------+
| Presentation | | Application | | Domain |
| Layer | | Layer | | Layer |
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| HTTP Handlers | | Use Cases | | Entities |
| Controllers | | Services | | Value Objects |
| Middleware | | DTOs | | Repositories |
| Validators | | Commands | | Domain Events |
| | | Queries | | |
+-------------------+ +-------------------+ +-------------------+
| | |
v v v
+-------------------+ +-------------------+ +-------------------+
| Infrastructure | | Integration | | |
| Layer | | Layer | | |
+-------------------+ +-------------------+
| | | |
| TypeORM Repos | | External APIs |
| Database | | Message Queues |
| File Storage | | Email Service |
| Caching | | Authentication |
| | | |
================================================================================

Organize your TypeScript project by feature or layer:

src/
├── main.ts # Application entry point
├── config/ # Configuration files
│ ├── database.ts
│ └── app.ts
├── entities/ # TypeORM entities (Domain Layer)
│ ├── user.entity.ts
│ └── product.entity.ts
├── repositories/ # Data access (Infrastructure Layer)
│ ├── user.repository.ts
│ └── product.repository.ts
├── services/ # Business logic (Application Layer)
│ ├── user.service.ts
│ └── product.service.ts
├── dto/ # Data Transfer Objects
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
├── controllers/ # HTTP handlers (Presentation Layer)
│ ├── user.controller.ts
│ └── product.controller.ts
├── middleware/ # Express/Fastify middleware
│ └── auth.middleware.ts
└── utils/ # Helper functions
└── validation.ts

Alternative structure organized by feature:

src/
├── features/
│ ├── users/
│ │ ├── entities/
│ │ │ └── user.entity.ts
│ │ ├── repositories/
│ │ │ └── user.repository.ts
│ │ ├── services/
│ │ │ └── user.service.ts
│ │ ├── controllers/
│ │ │ └── user.controller.ts
│ │ ├── dto/
│ │ │ └── create-user.dto.ts
│ │ └── index.ts # Barrel exports
│ └── products/
│ └── ...
├── shared/
│ ├── entities/
│ ├── repositories/
│ └── services/
└── config/

Terminal window
mkdir my-typeorm-app
cd my-typeorm-app
npm init -y
npm install typescript ts-node @types/node typeorm pg
npm install -D @types/node nodemon
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
src/config/database.ts
import 'reflect-metadata';
import { DataSource } from 'typeorm';
import { User } from '../entities/user.entity';
import { Product } from '../entities/product.entity';
export const AppDataSource = new DataSource({
type: 'postgres',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT) || 5432,
username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'password',
database: process.env.DB_NAME || 'mydb',
synchronize: process.env.NODE_ENV !== 'production',
logging: process.env.NODE_ENV === 'development',
entities: [User, Product],
migrations: ['src/migrations/*.ts'],
subscribers: [],
pool: {
max: 20,
min: 2,
acquire: 30000,
idle: 10000,
},
});
// Initialize connection
export async function initializeDatabase(): Promise<void> {
try {
await AppDataSource.initialize();
console.log('Database connection established');
} catch (error) {
console.error('Database connection failed:', error);
throw error;
}
}
src/entities/user.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
OneToMany,
} from 'typeorm';
import { Product } from './product.entity';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ length: 100 })
name: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@Column({ default: true })
isActive: boolean;
@OneToMany(() => Product, (product) => product.owner)
products: Product[];
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}
src/repositories/user.repository.ts
import { Repository, FindOptionsWhere, DeepPartial } from 'typeorm';
import { User } from '../entities/user.entity';
export class UserRepository {
constructor(private repository: Repository<User>) {}
async findAll(): Promise<User[]> {
return this.repository.find();
}
async findOneById(id: number): Promise<User | null> {
return this.repository.findOne({ where: { id } as FindOptionsWhere<User> });
}
async findByEmail(email: string): Promise<User | null> {
return this.repository.findOne({ where: { email } as FindOptionsWhere<User> });
}
async create(data: DeepPartial<User>): Promise<User> {
const user = this.repository.create(data);
return this.repository.save(user);
}
async update(id: number, data: Partial<User>): Promise<User> {
await this.repository.update(id, data);
return this.findOneById(id)!;
}
async delete(id: number): Promise<void> {
await this.repository.delete(id);
}
async findWithProducts(id: number): Promise<User | null> {
return this.repository.findOne({
where: { id } as FindOptionsWhere<User>,
relations: ['products'],
});
}
}
src/services/user.service.ts
import { UserRepository } from '../repositories/user.repository';
import { CreateUserDto } from '../dto/create-user.dto';
import { User } from '../entities/user.entity';
export class UserService {
constructor(private userRepository: UserRepository) {}
async getAllUsers(): Promise<User[]> {
return this.userRepository.findAll();
}
async getUserById(id: number): Promise<User | null> {
return this.userRepository.findOneById(id);
}
async getUserWithProducts(id: number): Promise<User | null> {
return this.userRepository.findWithProducts(id);
}
async createUser(dto: CreateUserDto): Promise<User> {
// Check if user exists
const existingUser = await this.userRepository.findByEmail(dto.email);
if (existingUser) {
throw new Error('User with this email already exists');
}
// Hash password (implement based on your needs)
const hashedPassword = await this.hashPassword(dto.password);
return this.userRepository.create({
...dto,
password: hashedPassword,
});
}
async updateUser(id: number, data: Partial<User>): Promise<User> {
const user = await this.userRepository.findOneById(id);
if (!user) {
throw new Error('User not found');
}
return this.userRepository.update(id, data);
}
async deleteUser(id: number): Promise<void> {
const user = await this.userRepository.findOneById(id);
if (!user) {
throw new Error('User not found');
}
await this.userRepository.delete(id);
}
private async hashPassword(password: string): Promise<string> {
// Implement password hashing (e.g., using bcrypt)
return password; // Placeholder
}
}

Step 7: Create Controller (Express.js example)

Section titled “Step 7: Create Controller (Express.js example)”
src/controllers/user.controller.ts
import { Request, Response, NextFunction } from 'express';
import { UserService } from '../services/user.service';
import { CreateUserDto } from '../dto/create-user.dto';
export class UserController {
constructor(private userService: UserService) {}
async getAllUsers(req: Request, res: Response, next: NextFunction) {
try {
const users = await this.userService.getAllUsers();
res.json({ success: true, data: users });
} catch (error) {
next(error);
}
}
async getUserById(req: Request, res: Response, next: NextFunction) {
try {
const id = parseInt(req.params.id);
const user = await this.userService.getUserById(id);
if (!user) {
return res.status(404).json({ success: false, message: 'User not found' });
}
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
}
async createUser(req: Request, res: Response, next: NextFunction) {
try {
const dto: CreateUserDto = req.body;
const user = await this.userService.createUser(dto);
res.status(201).json({ success: true, data: user });
} catch (error) {
next(error);
}
}
async updateUser(req: Request, res: Response, next: NextFunction) {
try {
const id = parseInt(req.params.id);
const user = await this.userService.updateUser(id, req.body);
res.json({ success: true, data: user });
} catch (error) {
next(error);
}
}
async deleteUser(req: Request, res: Response, next: NextFunction) {
try {
const id = parseInt(req.params.id);
await this.userService.deleteUser(id);
res.status(204).send();
} catch (error) {
next(error);
}
}
}
src/main.ts
import express, { Request, Response, NextFunction } from 'express';
import { AppDataSource, initializeDatabase } from './config/database';
import { UserController } from './controllers/user.controller';
import { UserService } from './services/user.service';
import { UserRepository } from './repositories/user.repository';
async function bootstrap() {
// Initialize database
await initializeDatabase();
// Set up repositories, services, controllers
const userRepository = new UserRepository(AppDataSource.getRepository('User'));
const userService = new UserService(userRepository);
const userController = new UserController(userService);
// Create Express app
const app = express();
app.use(express.json());
// Routes
app.get('/users', (req, res, next) => userController.getAllUsers(req, res, next));
app.get('/users/:id', (req, res, next) => userController.getUserById(req, res, next));
app.post('/users', (req, res, next) => userController.createUser(req, res, next));
app.put('/users/:id', (req, res, next) => userController.updateUser(req, res, next));
app.delete('/users/:id', (req, res, next) => userController.deleteUser(req, res, next));
// Error handling middleware
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ success: false, message: 'Internal server error' });
});
// Start server
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
}
bootstrap();

For smaller applications, implement simple dependency injection:

src/container.ts
class Container {
private services: Map<string, any> = new Map();
register<T>(key: string, instance: T): void {
this.services.set(key, instance);
}
resolve<T>(key: string): T {
const service = this.services.get(key);
if (!service) {
throw new Error(`Service ${key} not found`);
}
return service;
}
}
export const container = new Container();
// Usage
container.register('userRepository', new UserRepository(AppDataSource.getRepository('User')));
container.register('userService', new UserService(container.resolve<UserRepository>('userRepository')));

This chapter covered:

  • Application Architecture: Layered architecture patterns for TypeScript
  • Project Structure: Feature-based and layer-based organization
  • Complete Implementation: From database setup to HTTP controllers
  • Dependency Injection: Simple DI without frameworks

The key principles:

  1. Separate concerns across layers
  2. Use repositories for data access
  3. Keep business logic in services
  4. Controllers should be thin

Chapter 22: Service Layer Design


Last Updated: February 2026