Files
TeleUploader/docs/superpowers/plans/2026-07-28-ddd-clean-architecture-restructure.md
T
2026-07-28 17:31:11 +07:00

32 KiB

DDD & Clean Architecture Restructure Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Restructure TeleUploader from flat architecture to DDD/Clean Architecture with repository abstraction, controller/use-case separation, and full JSDoc documentation.

Architecture: 5-layer DDD — domain (entities + ports), application (use cases + DTOs), infrastructure (persistence + telegram + cache), interfaces (HTTP controllers + routes + middleware + bot + S3 protocol), and shared/config foundation.

Tech Stack: Bun + TypeScript + Drizzle ORM + Telegraf

Global Constraints

  • Zero behavioral changes — only move/restructure code, never alter logic
  • Old source files remain until Phase 8 (cleanup), so existing tests keep passing
  • JSDoc on every exported function, interface, type, and class (English)
  • All new imports use relative paths within new structure
  • config/ and shared/ have zero dependencies on other new layers
  • Domain ports/ interfaces are implemented by infrastructure/ repositories
  • Use cases constructed via factory functions accepting repository interfaces (no DI framework)
  • Controller = HTTP parsing + use case call + response formatting only; no business logic
  • Domain entities reuse Drizzle types (no pure domain models)
  • bun test test/<file> must pass at every phase (old files remain)

Phase 1: Foundation Layer — Config + Shared Utilities

Task 1: Create config/index.ts

Files:

  • Create: src/config/index.ts

Interfaces:

  • Produces: AppConfig type, config: AppConfig singleton

  • Create src/config/index.ts — Move content from src/env.ts. Same AppConfig interface, same config export. Add JSDoc to AppConfig interface (document every property), the config export, parseNumber, parseTokens, parseDomains, maskSecret, maskDatabaseUrl.

  • Run tests to verify nothing broken

bun test test/env.test.ts

Expected: PASS

  • Create src/config/__tests__/env.test.ts — Mirror of test/env.test.ts but imports from ../../config/index.ts

  • Run both env tests to verify

bun test test/env.test.ts
bun test src/config/__tests__/env.test.ts

Expected: Both PASS

  • Commit
git add src/config/
git commit -m "feat: create config layer with JSDoc"

Task 2: Create shared/ errors and logger

Files:

  • Create: src/shared/errors/index.ts
  • Create: src/shared/logger/index.ts

Interfaces:

  • Produces: DomainError, FileNotFoundError, BucketNotFoundError, FileTooLargeError, DuplicateFileError, AuthenticationError, ValidationError, logger singleton, type Logger

  • Create src/shared/errors/index.ts — Export all error classes from spec Section 11. Add JSDoc for each class.

  • Create src/shared/logger/index.ts — Move content from src/utils/logger.ts. Same implementation. Add JSDoc.

  • Quick compile check

bun build src/config/index.ts --target=bun --outfile=/dev/null 2>&1 | head -5
bun build src/shared/errors/index.ts --target=bun --outfile=/dev/null 2>&1 | head -5

Expected: No errors

  • Commit
git add src/shared/errors/ src/shared/logger/
git commit -m "feat: create shared errors and logger layer"

Task 3: Create shared/ metrics

Files:

  • Create: src/shared/metrics/index.ts

Interfaces:

  • Produces: metricsCollector, MetricsCollector, getSnapshot()

  • Create src/shared/metrics/index.ts — Move content from src/utils/metrics.ts. Same implementation. Add JSDoc to class, all methods, and getSnapshot() return type.

  • Compile check

bun build src/shared/metrics/index.ts --target=bun --outfile=/dev/null 2>&1 | head -5

Expected: No errors

  • Commit
git add src/shared/metrics/
git commit -m "feat: create shared metrics layer"

Task 4: Create shared/utils (file, ip, retry, zip)

Files:

  • Create: src/shared/utils/file.ts
  • Create: src/shared/utils/ip.ts
  • Create: src/shared/utils/retry.ts
  • Create: src/shared/utils/zip.ts

Interfaces:

  • Produces: getErrorMessage, cleanupTempFile, getFileType, checkFileSize, ensureExtension, extractFileName, extractMimeType, computeHash, extractFileFromMessage, detectFileType, getFileSizeLimit, formatCreatedAt, buildUploadResponse, extractClientIp, withRetry, withTimeout, withFallback, createZip, extractZipEntry, locateZipEntry

  • Create src/shared/utils/file.ts — Move all exports from src/utils/file.ts. Add JSDoc to every export. Keep all logic identical.

  • Create src/shared/utils/ip.ts — Move extractClientIp from src/utils/ip.ts. Add JSDoc.

  • Create src/shared/utils/retry.ts — Move all exports from src/utils/retry.ts. Add JSDoc.

  • Create src/shared/utils/zip.ts — Move all exports from src/utils/zip.ts. Add JSDoc.

  • Run existing tests to verify

bun test test/rateLimit.test.ts
bun test test/file.test.ts
bun test test/zip.test.ts

Expected: PASS

  • Commit
git add src/shared/utils/
git commit -m "feat: create shared utilities layer with JSDoc"

Phase 2: Domain Layer — Entities + Ports

Task 5: Create domain/entities (File, Bucket, FilePart, Multipart)

Files:

  • Create: src/domain/entities/file.ts
  • Create: src/domain/entities/bucket.ts
  • Create: src/domain/entities/file-part.ts
  • Create: src/domain/entities/multipart.ts

Interfaces:

  • Produces: File, NewFile, FilePart, NewFilePart, Bucket, MultipartUpload, MultipartPart types

  • Create src/domain/entities/file.ts — Re-export File and NewFile types from Drizzle schema. Add domain JSDoc.

// Re-export Drizzle types as domain entities
export type { File, NewFile } from '../../infrastructure/persistence/drizzle/schema';

Wait — this creates a circular dependency! Domain shouldn't import from infrastructure. Instead, define standalone interfaces:

/**
 * Core domain entity representing a file stored in Telegram.
 * Contains both Telegram metadata and optional S3-compatible fields.
 */
export interface File {
  id: string;
  publicId: string;
  telegramFileId: string;
  telegramFileUniqueId: string;
  storageChatId: number;
  storageMessageId: number;
  fileName: string;
  mimeType: string;
  sizeBytes: number;
  fileType: string;
  uploaderId: number;
  fileHash: string | null;
  archiveTelegramFileId: string | null;
  archiveStorageMessageId: number | null;
  archiveFileName: string | null;
  archiveEntryName: string | null;
  archiveMimeType: string | null;
  archiveSizeBytes: number | null;
  bucketId: string | null;
  s3Key: string | null;
  storageBackend: string | null;
  isDeleted: boolean | null;
  multipartUploadId: string | null;
  partCount: number | null;
  createdAt: Date;
  updatedAt: Date;
}

export type NewFile = Omit<File, 'id' | 'createdAt' | 'updatedAt'>;
  • Create src/domain/entities/bucket.ts — Standalone Bucket interface.

  • Create src/domain/entities/file-part.ts — Standalone FilePart + NewFilePart types with CompressionAlgorithm.

  • Create src/domain/entities/multipart.ts — Standalone MultipartUpload, MultipartPart types.

  • Compile check

bun build src/domain/entities/file.ts --target=bun --outfile=/dev/null 2>&1 | head -5

Expected: No errors

  • Commit
git add src/domain/entities/
git commit -m "feat: create domain entities with JSDoc"

Task 6: Create domain/ports (repository interfaces)

Files:

  • Create: src/domain/ports/file-repository.ts
  • Create: src/domain/ports/bucket-repository.ts
  • Create: src/domain/ports/file-part-repository.ts
  • Create: src/domain/ports/multipart-repository.ts

Interfaces:

  • Produces: IFileRepository, IBucketRepository, IFilePartRepository, IMultipartRepository, ITelegramService, S3FileRecord, CompressionAlgorithm

  • Create src/domain/ports/file-repository.ts — Define interface for all file operations currently in db/files.ts and db/files-ext.ts.

import type { File, NewFile } from '../entities/file';

export interface S3FileRecord extends File {
  bucketId: string;
  s3Key: string;
}

export interface IFileRepository {
  findByHash(hash: string): Promise<File | null>;
  findByPublicId(publicId: string): Promise<File | null>;
  findByUniqueId(telegramFileUniqueId: string): Promise<File | null>;
  findByBucketAndKey(bucketId: string, s3Key: string): Promise<File | null>;
  create(file: NewFile): Promise<File>;
  listByPrefix(
    bucketId: string,
    prefix: string,
    delimiter: string | null,
    maxKeys: number,
    startAfter: string | null,
  ): Promise<{ objects: S3FileRecord[]; prefixes: string[] }>;
  softDelete(bucketId: string, s3Key: string): Promise<boolean>;
  softDeleteBatch(bucketId: string, keys: string[]): Promise<number>;
  countByBucket(bucketId: string): Promise<number>;
  findOrphansByBucket(bucketId: string): Promise<File[]>;
}
  • Create src/domain/ports/bucket-repository.ts — Interface for bucket CRUD.

  • Create src/domain/ports/file-part-repository.ts — Interface for file parts operations.

  • Create src/domain/ports/multipart-repository.ts — Interface for multipart upload operations.

  • Create src/domain/ports/telegram-service.ts — Interface for Telegram operations.

/** Result of forwarding a file to Telegram storage */
export interface ForwardResult {
  telegramFileId: string;
  telegramFileUniqueId: string;
  storageMessageId: number;
}

/** File information returned by Telegram's getFile API */
export interface TelegramFileInfo {
  file_size: number;
  mime_type: string;
  file_path: string;
  bot_token: string;
}

/** Abstraction over Telegram bot API 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>;
}
  • Compile check
bun build src/domain/ports/file-repository.ts --target=bun --outfile=/dev/null 2>&1
bun build src/domain/ports/telegram-service.ts --target=bun --outfile=/dev/null 2>&1

Expected: No errors (domain has no infrastructure dependencies)

  • Commit
git add src/domain/ports/
git commit -m "feat: create domain port interfaces with JSDoc"

Phase 3: Infrastructure Layer — Persistence, Telegram, Cache

Task 7: Create infrastructure/persistence/drizzle (DB init, schema, migrate)

Files:

  • Create: src/infrastructure/persistence/drizzle/index.ts
  • Create: src/infrastructure/persistence/drizzle/schema.ts
  • Create: src/infrastructure/persistence/drizzle/migrate.ts

Interfaces:

  • Consumes: domain entities (for type alignment)

  • Produces: db client, files, fileParts table definitions, runMigration(), Drizzle schema types

  • Create src/infrastructure/persistence/drizzle/index.ts — Move content from src/db/index.ts. Same implementation. Add JSDoc to exports.

  • Create src/infrastructure/persistence/drizzle/schema.ts — Move content from src/db/schema.ts. Same implementation. Add JSDoc to table definitions and exported types.

  • Create src/infrastructure/persistence/drizzle/migrate.ts — Move content from src/db/migrate.ts. Same implementation. Add JSDoc.

  • Compile check

bun build src/infrastructure/persistence/drizzle/index.ts --target=bun --outfile=/dev/null 2>&1 | head -10

Expected: No errors

  • Commit
git add src/infrastructure/persistence/drizzle/
git commit -m "feat: create persistence drizzle layer"

Task 8: Create infrastructure/persistence/repositories (implement ports)

Files:

  • Create: src/infrastructure/persistence/repositories/file-repository.ts
  • Create: src/infrastructure/persistence/repositories/bucket-repository.ts
  • Create: src/infrastructure/persistence/repositories/file-part-repository.ts
  • Create: src/infrastructure/persistence/repositories/multipart-repository.ts

Interfaces:

  • Consumes: IFileRepository, IBucketRepository, IFilePartRepository, IMultipartRepository from domain/ports

  • Produces: Concrete repository classes implementing each interface

  • Create src/infrastructure/persistence/repositories/file-repository.ts — Implement IFileRepository using Drizzle. Extract logic from src/db/files.ts and src/db/files-ext.ts. Methods delegate to Drizzle queries (same SQL as original).

import { eq, and, sql } from 'drizzle-orm';
import { db, files as fileSchema } from '../drizzle/index';
import type { File, NewFile } from '../../../domain/entities/file';
import type { IFileRepository, S3FileRecord } from '../../../domain/ports/file-repository';

export class DrizzleFileRepository implements IFileRepository {
  async findByHash(hash: string): Promise<File | null> {
    const result = await db.select().from(fileSchema).where(eq(fileSchema.fileHash, hash)).limit(1);
    return result[0] || null;
  }
  // ... all methods from original files.ts + files-ext.ts
}
  • Create src/infrastructure/persistence/repositories/bucket-repository.ts — Implement IBucketRepository from src/db/buckets.ts.

  • Create src/infrastructure/persistence/repositories/file-part-repository.ts — Implement IFilePartRepository from src/db/file-parts.ts.

  • Create src/infrastructure/persistence/repositories/multipart-repository.ts — Implement IMultipartRepository from src/db/multipart.ts.

  • Compile check

bun build src/infrastructure/persistence/repositories/file-repository.ts --target=bun --outfile=/dev/null 2>&1 | head -10

Expected: No errors

  • Commit
git add src/infrastructure/persistence/repositories/
git commit -m "feat: create Drizzle repository implementations"

Task 9: Create infrastructure/telegram (bot-pool, queue, batcher, chunked-storage)

Files:

  • Create: src/infrastructure/telegram/types.ts
  • Create: src/infrastructure/telegram/bot-pool.ts
  • Create: src/infrastructure/telegram/upload-queue.ts
  • Create: src/infrastructure/telegram/upload-batcher.ts
  • Create: src/infrastructure/telegram/chunked-storage.ts

Interfaces:

  • Consumes: ITelegramService, IFilePartRepository, IFileRepository from domain/ports

  • Produces: DrizzleFileRepository, BotPool, etc.

  • Create src/infrastructure/telegram/types.ts — Extract Telegram-specific types from src/utils/telegram.ts (no longer inline).

  • Create src/infrastructure/telegram/bot-pool.ts — Move multi-bot pool + retry logic from src/utils/telegram.ts. Implement ITelegramService interface. Add JSDoc.

  • Create src/infrastructure/telegram/upload-queue.ts — Move P-Queue wrapper from src/utils/telegramQueue.ts. Add JSDoc.

  • Create src/infrastructure/telegram/upload-batcher.ts — Move batch upload logic from src/utils/uploadBatcher.ts. Add JSDoc. Accept repository interface instead of importing db directly.

  • Create src/infrastructure/telegram/chunked-storage.ts — Move chunked storage logic from src/utils/chunked-storage.ts. Accept repository interfaces. Add JSDoc.

  • Compile check

bun build src/infrastructure/telegram/bot-pool.ts --target=bun --outfile=/dev/null 2>&1 | head -10
bun build src/infrastructure/telegram/upload-batcher.ts --target=bun --outfile=/dev/null 2>&1 | head -10

Expected: No errors

  • Commit
git add src/infrastructure/telegram/
git commit -m "feat: create infrastructure telegram layer"

Task 10: Create infrastructure/cache

Files:

  • Create: src/infrastructure/cache/index.ts

Interfaces:

  • Produces: Cache<T>, fileInfoCache singleton

  • Create src/infrastructure/cache/index.ts — Move generic TTL cache from src/utils/cache.ts. Add JSDoc. Same implementation.

  • Compile check

bun build src/infrastructure/cache/index.ts --target=bun --outfile=/dev/null 2>&1 | head -5

Expected: No errors

  • Commit
git add src/infrastructure/cache/
git commit -m "feat: create infrastructure cache layer"

Phase 4: Application Layer — DTOs + Use Cases

Task 11: Create application/dto

Files:

  • Create: src/application/dto/upload.ts
  • Create: src/application/dto/file.ts
  • Create: src/application/dto/bucket.ts
  • Create: src/application/dto/s3.ts
  • Create: src/application/dto/auth.ts

Interfaces:

  • Produces: UploadInput, UploadOutput, FileInfoResponse, BucketResponse, S3ObjectResponse, AuthSession, etc.

  • Create src/application/dto/upload.ts — Define upload request/response DTOs.

/** Input for the upload file use case */
export interface UploadInput {
  tempPath: string;
  fileHash: string;
  fileName: string;
  mimeType: string;
  fileType: string;
  sizeBytes: number;
  uploaderId?: number;
  bucketId?: string | null;
  s3Key?: string | null;
}

/** Output from the upload file use case */
export interface UploadOutput {
  publicId: string;
  fileName: string;
  mimeType: string;
  sizeBytes: number;
  fileType: string;
  createdAt: Date;
  downloadUrl: string;
}
  • Create src/application/dto/file.ts — File info response DTOs.

  • Create src/application/dto/bucket.ts — Bucket CRUD DTOs.

  • Create src/application/dto/s3.ts — S3 operation DTOs (list, copy, multipart).

  • Create src/application/dto/auth.ts — Auth request/response DTOs (login input, session info).

  • Compile check

bun build src/application/dto/upload.ts --target=bun --outfile=/dev/null 2>&1 | head -5

Expected: No errors

  • Commit
git add src/application/dto/
git commit -m "feat: create application DTOs"

Task 12: Create application/use-cases (upload, get-file, auth)

Files:

  • Create: src/application/use-cases/upload-file.ts
  • Create: src/application/use-cases/get-file.ts
  • Create: src/application/use-cases/authenticate.ts

Interfaces:

  • Consumes: Repository interfaces from domain/ports, DTOs from application/dto

  • Produces: Factory functions returning use case closures

  • Create src/application/use-cases/upload-file.ts — Extract upload business logic from src/routes/upload.ts + src/utils/chunked-storage.ts. Factory function pattern:

import { nanoid } from 'nanoid';
import type { IFileRepository } from '../../domain/ports/file-repository';
import type { IFilePartRepository } from '../../domain/ports/file-part-repository';
import type { ITelegramService } from '../../domain/ports/telegram-service';
import type { AppConfig } from '../../config/index';
import type { UploadInput, UploadOutput } from '../dto/upload';

export interface UploadFileUseCaseDeps {
  fileRepo: IFileRepository;
  filePartRepo: IFilePartRepository;
  telegramService: ITelegramService;
  config: AppConfig;
}

export function createUploadFileUseCase(deps: UploadFileUseCaseDeps) {
  return async (input: UploadInput): Promise<UploadOutput> => {
    // 1. Check dedup (fileRepo.findByHash)
    // 2. Check file size limits
    // 3. Determine storage strategy: chunked vs single vs batch
    // 4. Store via telegramService
    // 5. Insert DB record via fileRepo
    // 6. Build and return UploadOutput
  };
}
  • Create src/application/use-cases/get-file.ts — Extract file redirect/stream logic from src/routes/files.ts.

  • Create src/application/use-cases/authenticate.ts — Extract login/logout/session logic from src/routes/auth.ts + src/utils/auth.ts.

  • Compile check

bun build src/application/use-cases/upload-file.ts --target=bun --outfile=/dev/null 2>&1 | head -15

Expected: No errors (may have non-functional code until controllers wired up — skip if not compile-ready)

  • Commit
git add src/application/use-cases/authenticate.ts src/application/use-cases/upload-file.ts src/application/use-cases/get-file.ts
git commit -m "feat: create application use cases (upload, get-file, auth)"

Task 13: Create application/use-cases (bucket, s3-object, multipart)

Files:

  • Create: src/application/use-cases/manage-bucket.ts
  • Create: src/application/use-cases/s3-object.ts
  • Create: src/application/use-cases/multipart-upload.ts

Interfaces:

  • Consumes: Repository interfaces from domain/ports, DTOs from application/dto

  • Produces: Factory functions for bucket CRUD, S3 object ops, multipart upload

  • Create src/application/use-cases/manage-bucket.ts — Extract bucket CRUD logic from src/routes/web-api.ts + src/routes/s3.ts.

  • Create src/application/use-cases/s3-object.ts — Extract S3 get/put/delete/copy logic from src/routes/s3.ts. This is the biggest extraction.

  • Create src/application/use-cases/multipart-upload.ts — Extract S3 multipart upload logic from src/routes/s3.ts.

  • Commit

git add src/application/use-cases/manage-bucket.ts src/application/use-cases/s3-object.ts src/application/use-cases/multipart-upload.ts
git commit -m "feat: create application use cases (bucket, s3-object, multipart)"

Phase 5: Interfaces Layer — Controllers, Middleware, Routes, S3, Bot

Task 14: Create interfaces/s3 protocol files

Files:

  • Create: src/interfaces/s3/auth.ts
  • Create: src/interfaces/s3/headers.ts
  • Create: src/interfaces/s3/object-stream.ts
  • Create: src/interfaces/s3/range.ts
  • Create: src/interfaces/s3/virtual-host.ts
  • Create: src/interfaces/s3/xml.ts

Interfaces:

  • Produces: SigV4 auth, S3 headers, object streaming, range parsing, virtual-host extraction, XML builders — all same as src/utils/s3/*

  • Create each file — Move content from src/utils/s3/auth.ts, src/utils/s3/headers.ts, etc. Add JSDoc to each exported function. Same implementations.

  • Compile check

bun build src/interfaces/s3/auth.ts --target=bun --outfile=/dev/null 2>&1 | head -5

Expected: No errors

  • Commit
git add src/interfaces/s3/
git commit -m "feat: create interfaces/s3 protocol layer"

Task 15: Create HTTP middleware (auth, rate-limit)

Files:

  • Create: src/interfaces/http/middleware/auth.ts
  • Create: src/interfaces/http/middleware/rate-limit.ts

Interfaces:

  • Produces: requireAuth, getAuthSession, withRateLimit, checkRateLimit, cleanupRateLimitCache

  • Create src/interfaces/http/middleware/auth.ts — Move auth middleware from src/utils/auth.ts. Update imports to use config from ../../config/index. Add JSDoc.

  • Create src/interfaces/http/middleware/rate-limit.ts — Move rate limiter from src/utils/rateLimit.ts. Add JSDoc.

  • Compile check

bun build src/interfaces/http/middleware/auth.ts --target=bun --outfile=/dev/null 2>&1 | head -10

Expected: No errors

  • Commit
git add src/interfaces/http/middleware/
git commit -m "feat: create HTTP middleware layer"

Task 16: Create HTTP controllers

Files:

  • Create: src/interfaces/http/controllers/upload-controller.ts
  • Create: src/interfaces/http/controllers/file-controller.ts
  • Create: src/interfaces/http/controllers/auth-controller.ts
  • Create: src/interfaces/http/controllers/home-controller.ts
  • Create: src/interfaces/http/controllers/health-controller.ts
  • Create: src/interfaces/http/controllers/s3-controller.ts
  • Create: src/interfaces/http/controllers/web-api-controller.ts

Interfaces:

  • Consumes: Use case factory functions from application layer

  • Produces: HTTP handler functions compatible with Bun.serve() routes

  • Create src/interfaces/http/controllers/upload-controller.ts — Extract upload HTTP handling from src/routes/upload.ts. Controller parses request, calls use case, formats response:

import { createUploadFileUseCase } from '../../../application/use-cases/upload-file';
// ... setup use case with dependencies
// ... handler functions for multipart and JSON upload paths
  • Create src/interfaces/http/controllers/file-controller.ts — Extract from src/routes/files.ts. Controller handles params, calls getFile use case, handles redirect/stream.

  • Create src/interfaces/http/controllers/auth-controller.ts — Extract from src/routes/auth.ts.

  • Create src/interfaces/http/controllers/home-controller.ts — Move from src/routes/home.ts.

  • Create src/interfaces/http/controllers/health-controller.ts — Move from src/routes/health.ts.

  • Create src/interfaces/http/controllers/s3-controller.ts — Extract S3 dispatching from src/routes/s3.ts. This is the largest extraction — split into clear sections (bucket ops, object ops, multipart ops). The 1179-line file becomes a focused controller that delegates to use cases.

  • Create src/interfaces/http/controllers/web-api-controller.ts — Extract from src/routes/web-api.ts.

  • Move src/home.html to src/interfaces/http/controllers/home.html — Update reference in home-controller.

  • Commit

git add src/interfaces/http/controllers/
git commit -m "feat: create HTTP controller layer"

Task 17: Create HTTP route definitions

Files:

  • Create: src/interfaces/http/routes/index.ts

Interfaces:

  • Consumes: All controller handler functions

  • Produces: Route table for Bun.serve()

  • Create src/interfaces/http/routes/index.ts — Define route table matching current src/index.ts routes. Each route maps to its controller handler. Add JSDoc.

import type { Server } from 'bun';
import { handleUpload } from '../controllers/upload-controller';
import { handleFileRedirect, handleFileInfo } from '../controllers/file-controller';
// ... other imports

/** Defines all HTTP routes for the application */
export const routes: Server['routes'] = {
  '/api/upload': { POST: handleUpload },
  '/f/:public_id': { GET: handleFileRedirect },
  '/file/:public_id/info': { GET: handleFileInfo },
  '/health': { GET: handleHealth },
  '/docs': { GET: handleSwaggerHtml },
  '/swagger.json': { GET: handleSwaggerJson },
  // ... etc
};
  • Commit
git add src/interfaces/http/routes/
git commit -m "feat: create HTTP route definitions"

Task 18: Create interfaces/bot

Files:

  • Create: src/interfaces/bot/handler.ts

Interfaces:

  • Consumes: ITelegramService, use cases

  • Produces: startBot() function for Telegram bot lifecycle

  • Create src/interfaces/bot/handler.ts — Move bot handler logic from src/bot.ts. Add JSDoc. Update imports to use new structure.

  • Commit

git add src/interfaces/bot/
git commit -m "feat: create bot interface layer"

Phase 6: Entry Point Rewire

Task 19: Rewrite src/index.ts

Files:

  • Modify: src/index.ts

  • Modify (delete): src/bot.ts

  • Rewrite src/index.ts — Update to import from new structure. Server bootstrap, S3 detection, route table from interfaces/http/routes/index, lifecycle management.

import { serve } from 'bun';
import { config } from './config/index';
import { startBot } from './interfaces/bot/handler';
import { routes } from './interfaces/http/routes/index';
import { isS3Request } from './interfaces/s3/auth';
import { handleS3Request } from './interfaces/http/controllers/s3-controller';
import { extractS3BucketFromHost } from './interfaces/s3/virtual-host';
import { fileInfoCache } from './infrastructure/cache/index';
import { cleanupRateLimitCache } from './interfaces/http/middleware/rate-limit';
import { logger } from './shared/logger/index';
import { metricsCollector } from './shared/metrics/index';
  • Remove src/bot.ts (replaced by src/interfaces/bot/handler.ts)

  • Run full test suite

bun test

Expected: All tests PASS (old src files still exist for backward compat)

  • Commit
git add src/index.ts src/interfaces/bot/handler.ts
git rm src/bot.ts
git commit -m "feat: rewire entry point to new architecture"

Phase 7: Test Restructure

Task 20: Create mirror test structure

Files:

  • Create: test/unit/domain/entities/ (entity type tests)

  • Create: test/unit/application/use-cases/ (use case tests)

  • Create: test/unit/shared/ (util tests)

  • Create: test/integration/interfaces/http/ (route tests)

  • Create: test/integration/infrastructure/ (repo tests)

  • Create test folder structure mirroring src/

mkdir -p test/unit/domain/entities \
  test/unit/application/use-cases \
  test/unit/shared \
  test/integration/interfaces/http \
  test/integration/infrastructure/persistence \
  test/integration/infrastructure/telegram
  • Move util tests — Copy test/env.test.tstest/unit/config/env.test.ts, update import path to src/config/index. Copy test/file.test.tstest/unit/shared/file.test.ts, update imports. Copy test/zip.test.tstest/unit/shared/zip.test.ts.

  • Move route tests — Copy test/upload.test.tstest/integration/interfaces/http/upload.test.ts, update imports. Copy test/files.test.tstest/integration/interfaces/http/files.test.ts. Copy test/auth-routes.test.ts, test/health.test.ts, test/swagger.test.ts.

  • Move S3 tests — Copy test/s3-auth.test.tstest/unit/interfaces/s3/auth.test.ts. Copy test/s3-range.test.ts, test/s3-operations.test.ts, test/s3-bucket-config.test.ts, test/s3-object-stream.test.ts.

  • Move infrastructure tests — Copy test/db.test.tstest/unit/infrastructure/persistence/drizzle/schema.test.ts. Copy test/chunked-storage.test.tstest/unit/infrastructure/telegram/chunked-storage.test.ts. Copy test/telegram.test.ts, test/telegramQueue.test.ts.

  • Keep original test files — Do NOT delete them yet. Original tests still reference old source paths and need to keep passing for now.

  • Update test runner scripts in package.json — Add test patterns for new test locations:

"test:new": "bun test test/unit/ test/integration/",
"test:all": "bun test && bun test test/unit/ test/integration/"
  • Run both old and new tests
bun test test/env.test.ts
bun test test/unit/config/env.test.ts

Expected: Both PASS

  • Commit
git add test/unit/ test/integration/ package.json
git commit -m "test: restructure tests to mirror new architecture"

Phase 8: Cleanup — Delete Old Files

Task 21: Remove old source directories

Files: Delete old source directories once new structure is verified.

  • Delete old directories (only after all tests pass with new imports)
rm -rf src/db src/routes src/utils src/env.ts src/bot.ts
  • Run full test suite to confirm nothing is broken
bun test
bun test test/unit/ test/integration/

Expected: All PASS

  • Commit
git rm -r src/db src/routes src/utils src/env.ts src/bot.ts
git commit -m "chore: remove legacy source directories after migration"

Task 22: Final verification

  • Full test suite
bun run test:all

Expected: All PASS

  • Build verification
bun run build

Expected: dist/index.js and dist/migrate.js compile without errors

  • Lint check
bunx biome check src test/unit test/integration

Expected: No errors

  • Final commit if any fixes were needed
git add -A
git commit -m "chore: final cleanup after architecture migration"

Spec Coverage Check

Spec Requirement Covered In
New folder structure Task 5-19 (all phases)
DDD layers Phase 2 (domain), Phase 4 (application), Phase 3 (infrastructure), Phase 5 (interfaces)
Repository interfaces Task 6
Repository implementations Task 8
Controller/use-case separation Task 12-13 (use cases), Task 16 (controllers)
Dependency injection (factory functions) Task 12
Telegram service interface Task 6 (ITelegramService port)
Bot moved to interfaces/bot Task 18
S3 protocol moved to interfaces/s3 Task 14
Config moved to config/ Task 1
Shared utilities moved to shared/ Task 2-4
Error classes Task 2
JSDoc on all exports All creation tasks
Test restructure Task 20
Zero behavior changes No logic altered — only moved/extracted
Delete old files Task 21
Final verification Task 22