14 KiB
DDD & Clean Architecture Restructure — TeleUploader (filedrop)
Date: 2026-07-28 Author: Claude Status: Draft
1. Motivation
The current codebase uses a flat structure (src/routes/, src/utils/, src/db/) that mixes concerns:
- Route handlers contain inline DB queries, Telegram API calls, and business logic
- Domain entity types (from Drizzle ORM) leak into all layers
- No clear separation between HTTP concerns, application logic, and infrastructure
- Large files (
routes/s3.tsat 1179 lines) are hard to maintain, test, and reason about
The goal is to restructure into Domain-Driven Design (DDD) with Clean Architecture principles, adding JSDoc documentation throughout, without changing behaviour or breaking existing tests.
2. Approach: Hybrid C
| Aspect | Decision |
|---|---|
| Depth | Structural DDD + repository abstraction + controller/use-case separation |
| Domain entities | Re-use Drizzle-generated types (pragmatic, not creating new pure domain models) |
| Dependency inversion | Repository interfaces defined in domain/ports/, implementations in infrastructure/ |
| Dependency injection | Simple factory functions (no DI framework) |
| Testing | Test files restructured alongside source files |
| Documentation | JSDoc on all public functions, classes, interfaces, and types |
| Language | English for code and JSDoc |
3. Layer Architecture
┌─────────────────────────────────────────────┐
│ interfaces/ (Delivery) │
│ HTTP controllers, routes, middleware │
│ Bot handlers │
│ S3 protocol (SigV4, XML, range, stream) │
├─────────────────────────────────────────────┤
│ application/ (Use Cases) │
│ Upload file, get file, auth, bucket ops, │
│ S3 object ops, multipart upload │
├────────────────────┬────────────────────────┤
│ domain/ │ infrastructure/ │
│ entities/ │ persistence/ │
│ ports/ (interfaces)│ telegram/ │
│ │ cache/ │
├────────────────────┴────────────────────────┤
│ config/ + shared/ │
└─────────────────────────────────────────────┘
Dependency Rule: Outer layers depend on inner layers, never the reverse. Domain knows nothing about HTTP, Drizzle, or Telegram.
4. Full Directory Structure
src/
├── index.ts # Entry point: server bootstrap, lifecycle
├── domain/
│ ├── entities/
│ │ ├── file.ts # File entity type (re-export from drizzle)
│ │ ├── bucket.ts # Bucket entity type
│ │ ├── file-part.ts # FilePart entity type
│ │ └── multipart.ts # MultipartUpload, MultipartPart types
│ └── ports/
│ ├── file-repository.ts # IFileRepository interface
│ ├── bucket-repository.ts # IBucketRepository interface
│ ├── file-part-repository.ts # IFilePartRepository interface
│ └── multipart-repository.ts # IMultipartRepository interface
├── application/
│ ├── dto/
│ │ ├── upload.ts # Upload request/response DTOs
│ │ ├── file.ts # File response DTOs
│ │ ├── bucket.ts # Bucket DTOs
│ │ ├── s3.ts # S3 operation DTOs
│ │ └── auth.ts # Auth DTOs
│ └── use-cases/
│ ├── upload-file.ts # Upload logic (multipart, JSON base64)
│ ├── get-file.ts # File redirect/stream logic
│ ├── manage-bucket.ts # Bucket CRUD logic
│ ├── s3-object.ts # S3 get/put/delete/copy logic
│ ├── multipart-upload.ts # S3 multipart upload logic
│ └── authenticate.ts # Login/logout/session logic
├── infrastructure/
│ ├── persistence/
│ │ ├── drizzle/
│ │ │ ├── index.ts # DB client + Drizzle init (from db/index.ts)
│ │ │ └── schema.ts # Drizzle table definitions (from db/schema.ts)
│ │ │ └── migrate.ts # Migration runner (from db/migrate.ts)
│ │ └── repositories/
│ │ ├── file-repository.ts # IFileRepository implementation (Drizzle)
│ │ ├── bucket-repository.ts # IBucketRepository implementation
│ │ ├── file-part-repository.ts# IFilePartRepository implementation
│ │ └── multipart-repository.ts# IMultipartRepository implementation
│ ├── telegram/
│ │ ├── bot-pool.ts # Multi-bot pool + retry (from utils/telegram.ts)
│ │ ├── upload-queue.ts # P-Queue wrapper (from utils/telegramQueue.ts)
│ │ ├── upload-batcher.ts # Batch upload (from utils/uploadBatcher.ts)
│ │ ├── chunked-storage.ts # Chunked file storage (from utils/chunked-storage.ts)
│ │ └── types.ts # Telegram-related types
│ └── cache/
│ └── index.ts # Generic TTL cache (from utils/cache.ts)
├── interfaces/
│ ├── http/
│ │ ├── controllers/
│ │ │ ├── upload-controller.ts # Upload HTTP handler
│ │ │ ├── file-controller.ts # File redirect + info handler
│ │ │ ├── auth-controller.ts # Login/logout/me handler
│ │ │ ├── home-controller.ts # Home dashboard handler
│ │ │ ├── health-controller.ts # Health check handler
│ │ │ ├── s3-controller.ts # S3-compatible API handler
│ │ │ └── web-api-controller.ts # JSON REST API handler
│ │ ├── middleware/
│ │ │ ├── auth.ts # requireAuth, getAuthSession
│ │ │ └── rate-limit.ts # Rate limiter
│ │ └── routes/
│ │ └── index.ts # All route definitions
│ ├── bot/
│ │ └── handler.ts # Telegram bot event handlers (from bot.ts)
│ └── s3/
│ ├── auth.ts # SigV4 verification (from utils/s3/auth.ts)
│ ├── headers.ts # CORS/response headers
│ ├── object-stream.ts # Part streaming for GetObject
│ ├── range.ts # HTTP Range header parser
│ ├── virtual-host.ts # Virtual-hosted bucket extraction
│ └── xml.ts # S3 XML response builders/parsers
├── config/
│ └── index.ts # Environment config (from env.ts)
├── shared/
│ ├── errors/
│ │ └── index.ts # Domain + application error classes
│ ├── logger/
│ │ └── index.ts # Winston logger (from utils/logger.ts)
│ ├── metrics/
│ │ └── index.ts # Metrics collector (from utils/metrics.ts)
│ └── utils/
│ ├── file.ts # File type, MIME, hash, extension (from utils/file.ts)
│ ├── ip.ts # Client IP extraction (from utils/ip.ts)
│ ├── retry.ts # Retry/timeout/fallback (from utils/retry.ts)
│ └── zip.ts # ZIP create/extract (from utils/zip.ts)
5. Dependency Injection Pattern
No DI framework. Use-case factories accept repository interfaces:
// domain/ports/file-repository.ts
export interface IFileRepository {
findByHash(hash: string): Promise<File | null>;
findByPublicId(publicId: string): Promise<File | null>;
findByUniqueId(uniqueId: string): Promise<File | null>;
findByBucketAndKey(bucketId: string, key: string): Promise<File | null>;
create(file: NewFile): Promise<File>;
softDelete(bucketId: string, key: string): Promise<boolean>;
listByPrefix(...): Promise<{ objects: File[]; prefixes: string[] }>;
}
// application/use-cases/upload-file.ts
export function createUploadFileUseCase(repos: {
fileRepo: IFileRepository;
filePartRepo: IFilePartRepository;
telegramService: ITelegramService;
config: AppConfig;
}) {
return async (input: UploadInput): Promise<UploadOutput> => {
// business logic here, using repos.xxx() not db directly
};
}
This keeps use cases testable — inject mock repositories in tests.
6. Controller / Use-Case Boundary
Controller responsibilities:
- Parse HTTP request (body, params, headers)
- Call use case
- Format HTTP response
Use case responsibilities:
- Business logic (file size checks, dedup, chunking decisions)
- Orchestrate infrastructure calls (Telegram, DB, cache)
- Return DTOs
Example flow for upload:
UploadController.handleUpload(req)
→ parse multipart form data
→ write to temp file
→ call uploadFileUseCase({ tempPath, fileName, mimeType, ... })
→ check dedup (fileRepo.findByHash)
→ check file size limits
→ if chunked: telegramService.uploadChunks(...), filePartRepo.create(...)
→ if batch: telegramService.enqueueBatch(...)
→ fileRepo.create(...)
→ return UploadOutput
→ format response (buildUploadResponse)
→ return Response
7. Migration Strategy
The restructure will be done in one pass per layer, with tests updated in lockstep:
- Create new folder structure
- Move
shared/utilities first (no dependencies on other layers) - Move
config/ - Move
domain/entities + ports - Move
infrastructure/persistence + telegram + cache - Move
application/use cases + DTOs - Move
interfaces/controllers + middleware + routes - Update
src/index.tsentry point - Restructure test files matching new layout
- Run all tests, fix import paths
- Delete old folders
8. Entity Architecture
┌──────────────────────────────────────────────┐
│ Domain Layer │
│ Entities: File, Bucket, FilePart, │
│ MultipartUpload, MultipartPart │
│ Ports: IFileRepository, IBucketRepository, │
│ IFilePartRepository, │
│ IMultipartRepository │
├──────────────────────────────────────────────┤
│ Application Layer │
│ UploadFileUseCase │
│ ├── depends on IFileRepository │
│ ├── depends on IFilePartRepository │
│ └── depends on ITelegramService │
│ │
│ S3ObjectUseCase │
│ ├── depends on IFileRepository │
│ ├── depends on IBucketRepository │
│ ├── depends on IMultipartRepository │
│ └── depends on ITelegramService │
└──────────────────────────────────────────────┘
9. Telegram Service Interface
A new ITelegramService interface in domain/ports/ to abstract Telegram operations:
export interface ITelegramService {
forwardToStorage(fileChunk: unknown, fileName: string, fileType: string): Promise<ForwardResult>;
getFileInfo(telegramFileId: string): Promise<TelegramFileInfo>;
enqueueUpload<T>(task: () => Promise<T>): Promise<T>;
}
Implementation stays in infrastructure/telegram/.
10. JSDoc Standards
Every exported function, interface, type, and class gets JSDoc:
/**
* Uploads a file to Telegram storage, handling chunking for large files
* and batch deduplication for small files.
*
* @param input - The prepared upload data and metadata
* @param input.tempPath - Path to the temporary file on disk
* @param input.fileName - Display name for the file
* @param input.mimeType - MIME type of the file
* @param input.fileType - Telegram file category (document, photo, etc.)
* @param input.sizeBytes - File size in bytes
* @returns The stored file entity with Telegram metadata
* @throws {FileTooLargeError} If file exceeds Telegram size limits
*/
11. Error Handling
Custom error classes replacing generic Error throws:
// shared/errors/index.ts
export class DomainError extends Error { constructor(msg: string) { super(msg); this.name = 'DomainError'; } }
export class FileNotFoundError extends DomainError {}
export class BucketNotFoundError extends DomainError {}
export class FileTooLargeError extends DomainError {}
export class DuplicateFileError extends DomainError {}
export class AuthenticationError extends DomainError {}
export class ValidationError extends DomainError {}
Use cases throw domain errors. Controllers catch and format HTTP responses.
12. Existing Files That Remain Unchanged
Files that are purely structural (no business logic restructuring needed):
| Old Path | New Path |
|---|---|
src/home.html |
src/interfaces/http/controllers/home.html (or alongside home controller) |
src/utils/s3/* |
src/interfaces/s3/* (moved as-is) |
src/db/migrate.ts |
src/infrastructure/persistence/drizzle/migrate.ts |
13. State After Migration
- Total source files: ~60 (was ~30) — more focused files, each with clear responsibility
- JSDoc coverage: 100% of exported APIs
- Test files: ~24, restructured to mirror source layout
- Behaviour changes: Zero. No logic is altered, only moved
- External interfaces: All API endpoints, S3 XML formats, and response shapes unchanged