The backend API and WebSocket server for Codergrounds. Built with Express.js and TypeScript, following a Simplified Layered Architecture with Dependency Injection.
This project follows a Simplified Layered Architecture that balances structure with pragmatism. Unlike traditional MVC or over-engineered Clean Architecture, this approach provides clear boundaries while remaining maintainable and scalable.
The architecture is organized into three main layers:
core/- Framework-agnostic business logic (use cases, interfaces)infra/- Framework-specific implementations (Express, database, cache)shared/- Pure utilities (no business logic, no framework dependencies)
src/
├── core/ # Framework-agnostic business logic
│ ├── interfaces/ # Contracts (interfaces for dependencies)
│ │ ├── repositories/ # Repository interfaces
│ │ ├── services/ # Service interfaces (if needed)
│ │ └── cache/ # Cache interfaces
│ └── useCases/ # Business logic (one use case per action)
│ ├── auth/ # Authentication use cases
│ ├── user/ # User management use cases
│ └── playground/ # Playground use cases
│
├── infra/ # Framework-specific implementations
│ ├── database/ # Database connection & repositories
│ ├── cache/ # Cache implementations (Redis)
│ ├── http/ # Express-specific code
│ │ ├── controllers/ # Thin controllers (HTTP handling only)
│ │ ├── routes/ # Route definitions
│ │ └── middlewares/ # Express middlewares
│ ├── mappers/ # Data transformation (DB → Domain)
│ └── realtime/ # WebSocket/Socket.io setup
│
├── shared/ # Pure utilities
│ ├── types/ # TypeScript types and interfaces
│ ├── errors/ # Custom error classes
│ ├── utils/ # Pure utility functions
│ ├── decorators/ # TypeScript decorators
│ ├── hofs/ # Higher-order functions
│ └── logger/ # Logger instance
│
├── config/ # Configuration files
├── container.ts # Dependency injection setup
├── app.ts # Express app setup
├── routes.ts # Route aggregator
└── index.ts # Server entry point
- Separation of Concerns: Each layer has a clear responsibility
- Dependency Inversion: Core depends on abstractions (interfaces), not implementations
- Framework Independence: Core layer can be tested without Express or database
- Single Responsibility: One use case = one business action
- DRY: No duplicate implementations
index.ts → app.ts → routes.ts
↓
infra/http/ (routes → controllers → useCases)
↓
core/useCases/ (business logic, uses interfaces)
↓
infra/database/repositories/ (implements interfaces)
Dependency Rules:
core/→ Can importshared/✅infra/→ Can importcore/andshared/✅shared/→ Can only import fromshared/or external packages ✅- NEVER create circular dependencies
This project uses tsyringe for Dependency Injection, providing:
- Testability: Easy to mock dependencies
- Flexibility: Swap implementations without changing code
- Maintainability: Clear dependency relationships
- Interfaces define contracts in
core/interfaces/ - Implementations live in
infra/ - Registration happens in
container.ts - Resolution uses tokens from
shared/utils/container.utils.ts
// 1. Define interface
interface UserRepositoryInterface {
findById(id: string): Promise<User>;
}
// 2. Implement interface
class UserRepository implements UserRepositoryInterface { ... }
// 3. Register in container.ts
container.register<UserRepositoryInterface>(
ContainerTokens.userRepository,
{ useClass: UserRepository }
);
// 4. Use in use case
@injectable()
class LoginUseCase {
constructor(
@inject(ContainerTokens.userRepository)
private userRepo: UserRepositoryInterface
) {}
}- Express.js: REST API framework
- Socket.io: Real-time collaboration and chat
- Zod: Runtime validation (schemas shared with frontend)
- PostgreSQL: Primary data store
- Redis: Session store, caching, and BullMQ job queues
- tsyringe: Dependency Injection container
- TypeScript: Type-safe development
pnpm dev: Start development server with watch modepnpm build: Compile TypeScript todist/pnpm test: Run unit and integration tests via Vitestpnpm lint: Lint code with ESLintpnpm migrate:create <name>: Create a new migration file (e.g.,pnpm migrate:create add-user-avatar)pnpm migrate:up: Run database migrationspnpm migrate:down: Rollback database migrationspnpm seed: Seed database with development data
- Install dependencies:
pnpm install - Set up environment variables: Copy
.env.exampleto.envand configure - Run migrations:
pnpm migrate:up - Seed database (optional):
pnpm seed - Start development server:
pnpm dev
All API routes are versioned under /api/v1/