Architecture Overview
Express Base API follows a clean, modular MVC (Model-View-Controller) architecture with additional layers for separation of concerns.
Architecture Layers
┌─────────────────────────────────────┐
│ Client Application │
└──────────────┬──────────────────────┘
│ HTTP Requests
┌──────────────▼──────────────────────┐
│ Routes + Middleware │
│ (Authentication, Validation, │
│ Rate Limiting, Logging) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Controllers │
│ (Request handling, Response │
│ formatting) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ Services │
│ (Business logic, Database │
│ operations, Caching) │
└──────────────┬───────────────── ─────┘
│
┌──────────────▼──────────────────────┐
│ Models (Mongoose) │
│ (Database schemas, │
│ Validation) │
└──────────────┬──────────────────────┘
│
┌──────────────▼──────────────────────┐
│ MongoDB Database │
└─────────────────────────────────────┘
Cache Layer (Redis)
┌───────────────────────────────── ────┐
│ Caching Service │
│ - Authentication cache │
│ - Query result cache │
│ - Rate limiting │
└─────────────────────────────────────┘
Queue System (Bull)
┌─────────────────────────────────────┐
│ Background Jobs │
│ - Email sending │
│ - Data processing │
└─────────────────────────────────────┘
Directory Structure
express-backend-ts/
├── bin/ # CLI tools and scripts
│ ├── cli # Main CLI entry point
│ ├── script/ # Generation scripts
│ └── templates.js # Code templates
├── configs/ # Configuration files
│ ├── app.config.ts # App-wide configuration
│ ├── database.config.ts # MongoDB configuration
│ ├── redis.config.ts # Redis configuration
│ └── response.config.ts # Response formatting
├── docs/ # API documentation
│ ├── swagger.ts # Swagger configuration
│ └── routes/ # Route documentation
├── routes/ # Route definitions
│ ├── routes.ts # Main route registry
│ ├── admin/ # Admin routes
│ ├── app/ # Application routes
│ └── auth.route.ts # Authentication routes
├── src/ # Source code
│ ├── controllers/ # Request handlers
│ │ ├── admin/
│ │ ├── app/
│ │ └── auth.controller.ts
│ ├── models/ # Database models
│ │ ├── auth/
│ │ └── log.model.ts
│ ├── services/ # Business logic
│ │ ├── admin/
│ │ ├── app/
│ │ └── auth/
│ └── types/ # TypeScript interfaces
│ ├── service.types.ts
│ └── auth.types.ts
├── utils/ # Utilities and helpers
│ ├── bases/ # Base classes
│ │ ├── base.controller.ts
│ │ ├── base.service.ts
│ │ └── mail.service.ts
│ ├── helpers/ # Helper functions
│ │ ├── cache.helper.ts
│ │ └── logger.helper.ts
│ ├── interceptors/ # Request/response interceptors
│ ├── middlewares/ # Express middlewares
│ │ ├── app.middleware.ts
│ │ └── auth/
│ ├── seeders/ # Database seeders
│ └── validations/ # Joi validation schemas
├── templates/ # Email templates
│ └── auth/
├── main.ts # Application entry point
├── package.json
└── tsconfig.json
Request Flow
1. HTTP Request Arrives
GET /product/123
Headers:
x-api-key: xxx
Authorization: Bearer yyy
2. Middleware Stack
// Route definition in routes.ts
{
path: '/product',
middlewares: [
Middleware.xApiKey, // Validate API key
Middleware.rateLimit, // Check rate limits
Middleware.isLogin, // Authenticate user
]
}
Execution order:
- API Key Validation - Checks
x-api-keyheader - Rate Limiting - Prevents abuse (Redis-based)
- Authentication - Validates JWT token (cache-first)
- Request Logging - Logs request details
3. Controller Layer
// routes/app/product.route.ts
import ProductController from '../../src/controllers/app/product.controller';
const productController = new ProductController();
router.get('/:id', productController.show);
Controller responsibilities:
- Extract request parameters
- Call appropriate service method
- Format response
- Handle errors
4. Service Layer
// src/services/app/product.service.ts
class ProductService extends BaseService<IProduct> {
async findById(id: string) {
// Check cache first
const cached = await this.cacheGet(`product:${id}`);
if (cached) return { error: false, data: cached };
// Query database
const result = await this.model.findById(id);
// Cache result
await this.cacheSet(`product:${id}`, result, 3600);
return { error: false, data: result };
}
}
Service responsibilities:
- Business logic implementation
- Database operations
- Caching
- Data transformation
- Transaction management
5. Model Layer
// src/models/product.model.ts
const productSchema = {
name: { type: String, required: true },
price: { type: Number, required: true },
stock: { type: Number, default: 0 },
};
const Product: Model<IProduct> = BaseSchema<IProduct>(
'products',
productSchema
);
Model responsibilities:
- Schema definition
- Validation rules
- Indexes
- Virtual properties
- Static methods
6. Response
{
"status_code": 200,
"status": "SUCCESS",
"message": "Product retrieved successfully",
"data": {
"_id": "65abc123...",
"name": "Laptop",
"price": 999.99,
"stock": 10
}
}
Design Patterns
Singleton Pattern
Controllers and services use singleton pattern for optimal memory usage:
// Only one instance per service
class ProductService extends BaseService<IProduct> {
private static instance: ProductService;
static getInstance() {
if (!ProductService.instance) {
ProductService.instance = new ProductService(Product);
}
return ProductService.instance;
}
}
export default ProductService.getInstance();
Benefits:
- Reduced memory footprint
- Shared state across requests
- Database connection pooling
Repository Pattern
BaseService acts as a repository with common CRUD operations:
class BaseService<T> {
async find(query, options?): Promise<QueryResult<T[]>>
async findById(id: string): Promise<QueryResult<T>>
async create(data): Promise<QueryResult<T>>
async update(query, data): Promise<QueryResult<T>>
async delete(query): Promise<QueryResult<T>>
}
Dependency Injection
Services are injected into controllers:
class ProductController extends BaseController {
constructor() {
super(productService);
}
}
Cache-Aside Pattern
Services use cache-aside for optimal performance:
// Check cache → Cache miss → Query DB → Cache result
const data = await this.cacheGetOrSet(
cacheKey,
async () => await this.find(query),
ttl
);
Key Principles
Separation of Concerns
Each layer has a single responsibility:
| Layer | Responsibility | Should NOT |
|---|---|---|
| Routes | Define endpoints, apply middleware | Contain business logic |
| Controllers | Handle HTTP, format responses | Access database directly |
| Services | Business logic, database operations | Parse HTTP requests |
| Models | Data schema, validation | Contain business logic |
DRY (Don't Repeat Yourself)
- BaseController - Common controller methods
- BaseService - Common CRUD operations
- Base schemas - Automatic timestamps, soft delete
- Response formatting - Centralized response structure
Type Safety
Full TypeScript support with strict mode:
interface IProduct extends BaseDocument {
name: string;
price: number;
stock: number;
}
class ProductService extends BaseService<IProduct> {
// Full type inference and checking
}
Error Handling
Centralized error handling throughout the stack:
try {
const result = await service.create(data);
if (result.error) {
return response.error(res, result.message, 400);
}
return response.success(res, result.data, 201);
} catch (error) {
logger.error('Error creating product', error);
return response.error(res, 'Internal server error', 500);
}
Performance Optimizations
Caching Strategy
-
Authentication Cache (1-hour TTL)
- User + Auth data cached on login
- Cache-first authentication middleware
-
Query Cache (configurable TTL)
- Frequently accessed data cached
- Automatic invalidation on updates
-
Rate Limit Cache (Redis)
- Track request counts per IP/user
- Sliding window algorithm
Database Optimizations
- Indexes - Critical fields indexed
- Pagination - Limit query results
- Projection - Select only needed fields
- Population - Efficient reference loading
Background Processing
Queue system for non-blocking operations:
// Email sending in background
await mailQueue.add('send-email', {
to: user.email,
template: 'activation',
data: { token: activationToken }
});
Scalability Considerations
Horizontal Scaling
- Stateless design - No session storage in memory
- Redis cache - Shared across instances
- Bull queue - Distributed job processing
- MongoDB - Replication and sharding support
Vertical Scaling
- Connection pooling - Efficient database connections
- Memory management - Singleton pattern
- Async/await - Non-blocking operations
- Stream processing - Large file handling
Security Architecture
Defense in Depth
Multiple security layers:
- API Key Validation - First line of defense
- Rate Limiting - Prevent abuse
- JWT Authentication - User verification
- Input Validation - Joi schemas
- SQL/NoSQL Injection - Mongoose sanitization
- XSS Protection - Helmet.js
- CORS - Origin control
Authentication Flow
1. Login → JWT Access Token (1h) + Refresh Token (7d)
2. Request → Middleware checks cache → Validate token
3. Token expired → Use refresh token → New access token
4. Refresh expired → Force re-login
5. Logout → Invalidate cache + delete refresh token
Next Steps
- Base Classes - Deep dive into BaseService and BaseController
- Singleton Pattern - Understanding memory optimization
- Error Handling - Comprehensive error management
Questions? Check the GitHub Discussions or open an issue.