- Introduction
- Features
- Project Structure
- Getting Started
- Architecture
- API Documentation
- Database
- Caching
- Messaging
- Cross-Cutting Concerns
- Testing
- Deployment
- Contributing
- License
This project is a robust Node.js/TypeScript API template implementing clean architecture principles. It provides a solid foundation for building scalable, maintainable, and testable backend applications.
- TypeScript: Strongly typed language for better developer experience and fewer runtime errors
- Express.js: Fast, unopinionated, minimalist web framework for Node.js
- Clean Architecture: Clear separation of concerns for better maintainability
- MongoDB with Mongoose: Flexible and powerful ODM for MongoDB
- Redis Caching: Improved performance with in-memory data caching
- RabbitMQ Messaging: Reliable message queuing for asynchronous processing
- Swagger Documentation: Interactive API documentation
- Jest Testing: Comprehensive unit and integration testing setup
- Docker Support: Containerization for consistent development and deployment environments
- Logging (Winston): Advanced logging capabilities
- Error Handling: Centralized error handling and custom error classes
- Metrics (Prometheus): Application metrics collection and exposure
- Distributed Tracing (OpenTelemetry): Request tracing across services
- ESLint & Prettier: Code linting and formatting for consistent style
- Husky & lint-staged: Pre-commit hooks for code quality
- CI/CD: Sample GitHub Actions workflow
.
├── src/
│ ├── application/ # Application business logic
│ ├── domain/ # Domain entities and interfaces
│ ├── infrastructure/ # External services implementations
│ ├── interfaces/ # Controllers, routes
│ ├── cross_cutting/ # Cross-cutting concerns (logging, error handling, etc.)
│ ├── config/ # Configuration files
│ └── server.ts # Express app setup
├── tests/ # Test files
├── docs/ # Additional documentation
├── scripts/ # Utility scripts
├── .env.example # Example environment variables
├── .eslintrc.js # ESLint configuration
├── .prettierrc # Prettier configuration
├── jest.config.js # Jest configuration
├── tsconfig.json # TypeScript configuration
├── Dockerfile # Docker configuration
├── docker-compose.yml # Docker Compose configuration
└── README.md # Project documentation
- Node.js (v14+)
- npm or yarn
- Docker and Docker Compose (for containerized development)
- MongoDB
- Redis
- RabbitMQ
-
Clone the repository:
git clone https://github.com/yourusername/your-repo-name.git cd your-repo-name
-
Install dependencies:
npm install
-
Set up environment variables:
cp .env.example .env
Edit the
.env
file with your specific configuration. -
Start the development server:
npm run dev
Configuration is managed through environment variables and the config
directory. Key configuration files:
src/config/app.ts
: Application-wide settingssrc/config/database.ts
: Database connection settingssrc/config/cache.ts
: Caching configurationsrc/config/messaging.ts
: Message queue settingssrc/config/observability.ts
: Logging, metrics, and tracing configuration
This project follows clean architecture principles, separating the codebase into layers:
- Domain Layer: Core business logic and entities
- Application Layer: Use cases and application-specific business rules
- Infrastructure Layer: Implementations of interfaces defined in the domain layer
- Interfaces Layer: Controllers, routes, and adapters for external agencies
Dependency injection is implemented using tsyringe
. Example usage:
// src/infrastructure/repositories/UserRepository.ts
import { injectable } from 'tsyringe';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
@injectable()
export class UserRepository implements IUserRepository {
// Implementation
}
// src/application/services/UserService.ts
import { injectable, inject } from 'tsyringe';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
@injectable()
export class UserService {
constructor(
@inject('IUserRepository') private userRepository: IUserRepository
) {}
// Service methods
}
// src/interfaces/http/controllers/UserController.ts
import { injectable, inject } from 'tsyringe';
import { UserService } from '../../../application/services/UserService';
@injectable()
export class UserController {
constructor(
@inject(UserService) private userService: UserService
) {}
// Controller methods
}
API documentation is generated using Swagger. Access the Swagger UI at http://localhost:3000/api-docs
when running the application.
To document an API endpoint, use Swagger JSDoc comments:
/**
* @swagger
* /users:
* get:
* summary: Retrieve a list of users
* responses:
* 200:
* description: A list of users
* content:
* application/json:
* schema:
* type: array
* items:
* $ref: '#/components/schemas/User'
*/
router.get('/users', userController.getUsers);
This template uses MongoDB with Mongoose as the ODM. Database operations are abstracted through repository interfaces.
Example usage:
// src/infrastructure/repositories/MongoUserRepository.ts
import { injectable } from 'tsyringe';
import { IUserRepository } from '../../domain/repositories/IUserRepository';
import { UserModel } from '../models/UserModel';
@injectable()
export class MongoUserRepository implements IUserRepository {
async findById(id: string): Promise<User | null> {
return UserModel.findById(id).exec();
}
// Other repository methods
}
Redis is used for caching. The caching layer is implemented in the infrastructure layer and can be injected into services.
Example usage:
// src/infrastructure/cache/RedisCache.ts
import { injectable } from 'tsyringe';
import { createClient } from 'redis';
import { CACHE_CONFIG } from '../../config/cache';
@injectable()
export class RedisCache implements ICache {
private client;
constructor() {
this.client = createClient(CACHE_CONFIG);
this.client.connect();
}
async get(key: string): Promise<string | null> {
return this.client.get(key);
}
async set(key: string, value: string, ttl?: number): Promise<void> {
await this.client.set(key, value, { EX: ttl });
}
// Other cache methods
}
// Usage in a service
@injectable()
export class UserService {
constructor(
@inject('IUserRepository') private userRepository: IUserRepository,
@inject('ICache') private cache: ICache
) {}
async getUserById(id: string): Promise<User | null> {
const cachedUser = await this.cache.get(`user:${id}`);
if (cachedUser) {
return JSON.parse(cachedUser);
}
const user = await this.userRepository.findById(id);
if (user) {
await this.cache.set(`user:${id}`, JSON.stringify(user), 3600); // Cache for 1 hour
}
return user;
}
}
RabbitMQ is used for message queuing. The messaging layer is implemented in the infrastructure layer.
Example usage:
// src/infrastructure/messaging/RabbitMQClient.ts
import { injectable } from 'tsyringe';
import { connect, Connection, Channel } from 'amqplib';
import { MESSAGING_CONFIG } from '../../config/messaging';
@injectable()
export class RabbitMQClient implements IMessageQueue {
private connection: Connection;
private channel: Channel;
async initialize(): Promise<void> {
this.connection = await connect(MESSAGING_CONFIG.rabbitMQ.url);
this.channel = await this.connection.createChannel();
}
async publishMessage(queue: string, message: string): Promise<void> {
await this.channel.assertQueue(queue, { durable: true });
this.channel.sendToQueue(queue, Buffer.from(message));
}
// Other messaging methods
}
// Usage in a service
@injectable()
export class NotificationService {
constructor(
@inject('IMessageQueue') private messageQueue: IMessageQueue
) {}
async sendNotification(userId: string, message: string): Promise<void> {
await this.messageQueue.publishMessage('notifications', JSON.stringify({ userId, message }));
}
}
Logging is implemented using Winston. A custom logger is created in src/cross_cutting/logging/logger.ts
.
Example usage:
import { logger } from '../../cross_cutting/logging/logger';
logger.info('User logged in', { userId: user.id });
logger.error('Failed to process payment', { error: err.message, userId: user.id });
Centralized error handling is implemented in src/cross_cutting/error_handling/errorHandler.ts
.
Example usage:
import { AppError, asyncHandler } from '../../cross_cutting/error_handling';
export const createUser = asyncHandler(async (req: Request, res: Response) => {
const { email } = req.body;
const existingUser = await userService.findByEmail(email);
if (existingUser) {
throw new AppError('User already exists', 400);
}
// Create user logic
});
Prometheus is used for metrics collection. Metrics are set up in src/cross_cutting/metrics/metricsCollector.ts
.
Example usage:
import { metricsCollector } from '../../cross_cutting/metrics/metricsCollector';
// In your route handler
metricsCollector.httpRequestsTotal.inc({ method: req.method, path: req.path });
// Custom metric
const databaseQueryDuration = metricsCollector.createHistogram({
name: 'database_query_duration_seconds',
help: 'Duration of database queries in seconds',
labelNames: ['operation'],
});
const end = databaseQueryDuration.startTimer();
// Perform database query
end({ operation: 'findUser' });
Distributed tracing is implemented using OpenTelemetry. Tracing is set up in src/cross_cutting/tracing/tracer.ts
.
Example usage:
import { tracer } from '../../cross_cutting/tracing/tracer';
const span = tracer.startSpan('createUser');
try {
// User creation logic
span.setStatus({ code: SpanStatusCode.OK });
} catch (error) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
Jest is used for testing. Run tests with:
npm test
Example test:
// src/application/services/__tests__/UserService.test.ts
import { container } from 'tsyringe';
import { UserService } from '../UserService';
import { MockUserRepository } from '../../__mocks__/MockUserRepository';
describe('UserService', () => {
let userService: UserService;
let mockUserRepository: MockUserRepository;
beforeEach(() => {
mockUserRepository = new MockUserRepository();
container.registerInstance('IUserRepository', mockUserRepository);
userService = container.resolve(UserService);
});
it('should create a user', async () => {
const userData = { name: 'John Doe', email: '[email protected]' };
await userService.createUser(userData);
expect(mockUserRepository.create).toHaveBeenCalledWith(userData);
});
});
This project includes a Dockerfile and docker-compose.yml for containerized deployment.
Build and run with Docker Compose:
docker-compose up --build
For production deployment, consider using Kubernetes or a managed container service like AWS ECS.
- Fork the repository
- Create your feature branch (
git checkout -b feature/AmazingFeature
) - Commit your changes (
git commit -m 'Add some AmazingFeature'
) - Push to the branch (
git push origin feature/AmazingFeature
) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.