architecture

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Project Architecture and File Organization

项目架构与文件组织

This file is the SINGLE SOURCE OF TRUTH for folder structures and file naming conventions.
本文件是文件夹结构与文件命名规范的唯一权威来源

1. High-Level Architecture

1. 高层架构

This project MUST be structured as a Modular Monolith built with Next.js 16+ (App Router). It strictly adheres to Domain-Driven Design (DDD) and Clean Architecture principles.
Instead of organizing files exclusively by technical responsibilities (e.g., all controllers together, all models together), you MUST divide the root of
src/
into Bounded Contexts (feature modules) such as
auth
,
folders
,
short-urls
,
users
, and
workspaces
.
本项目必须采用基于Next.js 16+(App Router)构建的Modular Monolith架构,严格遵循领域驱动设计(DDD)Clean Architecture原则。
不能仅按技术职责组织文件(例如将所有控制器放在一起、所有模型放在一起),必须将
src/
根目录划分为Bounded Contexts(功能模块),如
auth
folders
short-urls
users
workspaces

2. Bounded Context Structure (The 4 Layers)

2. Bounded Context结构(四层架构)

Every module inside
src/
(except
app/
and
shared/
) MUST follow a strict 4-layer architecture. Dependencies MUST always point inwards toward the Domain.
text
src/<bounded-context>/
├── domain/            # 1. CORE: Pure typescript. No frameworks. No DB imports.
│   ├── entities/      # Rich domain models with getters and .toSnapshot() rules.
│   ├── value-objects/ # Immutable primitives (e.g., folderName.ts) with validation.
│   ├── errors/        # Domain errors (e.g., folderAlreadyExistsError.ts).
│   └── interfaces/    # Contracts (Abstract Classes) for Repositories.
├── use-cases/         # 2. APPLICATION: Orchestration layer.
│   ├── createFolder.ts      # Specific business flows. Uses repositories and domain models.
│   └── createFolder.test.ts # Co-located unit tests.
├── infrastructure/    # 3. INFRASTRUCTURE: External concerns. Database, Email, etc.
│   ├── postgresFolderRepository.ts                  # Implementation of domain interfaces.
│   └── postgresFolderRepository.integration.test.ts # Co-located integration tests.
└── presentation/      # 4. PRESENTATION: Next.js and UI concerns.
    ├── actions/       # Server Actions (e.g., createFolderAction.ts).
    ├── components/    # React Components (e.g., FolderList.tsx).
    └── hooks/         # React Hooks.
src/
内的每个模块(除
app/
shared/
外)必须遵循严格的四层架构。依赖关系必须始终向内指向领域层
text
src/<bounded-context>/
├── domain/            # 1. 核心层:纯TypeScript代码,无框架依赖,无数据库导入。
│   ├── entities/      # 包含getter和.toSnapshot()规则的富领域模型。
│   ├── value-objects/ # 带验证逻辑的不可变基础类型(如folderName.ts)。
│   ├── errors/        # 领域错误(如folderAlreadyExistsError.ts)。
│   └── interfaces/    # 仓库接口(抽象类)。
├── use-cases/         # 2. 应用层:编排层。
│   ├── createFolder.ts      # 特定业务流程,使用仓库和领域模型。
│   └── createFolder.test.ts # 同目录单元测试。
├── infrastructure/    # 3. 基础设施层:外部依赖相关,如数据库、邮件服务等。
│   ├── postgresFolderRepository.ts                  # 领域接口的实现。
│   └── postgresFolderRepository.integration.test.ts # 同目录集成测试。
└── presentation/      # 4. 表现层:Next.js与UI相关内容。
    ├── actions/       # Server Actions(如createFolderAction.ts)。
    ├── components/    # React Components(如FolderList.tsx)。
    └── hooks/         # React Hooks。

3. Naming Conventions and Locations

3. 命名规范与位置

You MUST adhere to the following file naming and location conventions across all Bounded Contexts:
ConceptLocationNaming ConventionExample
Bounded Context
src/<context>/
kebab-case
src/short-urls/
Value Objects
domain/value-objects/
camelCase.ts
folderName.ts
Entities
domain/entities/
camelCase.ts
folder.ts
Domain Errors
domain/errors/
camelCase.ts
(suffix
Error
)
folderNotFoundError.ts
Interfaces
domain/interfaces/
camelCase.ts
(suffix
Repository
)
folderRepository.ts
Use Cases
use-cases/
camelCase.ts
(verb first)
createFolder.ts
Infrastructure
infrastructure/
camelCase.ts
(tech prefix)
postgresFolderRepository.ts
Server Actions
presentation/actions/
camelCase.ts
(suffix
Action
)
createFolderAction.ts
Components
presentation/components/
PascalCase.tsx
FolderList.tsx
所有Bounded Context必须遵循以下文件命名与位置规范:
概念位置命名规范示例
Bounded Context
src/<context>/
kebab-case
src/short-urls/
值对象
domain/value-objects/
camelCase.ts
folderName.ts
实体
domain/entities/
camelCase.ts
folder.ts
领域错误
domain/errors/
camelCase.ts
(后缀
Error
folderNotFoundError.ts
接口
domain/interfaces/
camelCase.ts
(后缀
Repository
folderRepository.ts
用例
use-cases/
camelCase.ts
(动词开头)
createFolder.ts
基础设施实现
infrastructure/
camelCase.ts
(技术前缀)
postgresFolderRepository.ts
Server Actions
presentation/actions/
camelCase.ts
(后缀
Action
createFolderAction.ts
组件
presentation/components/
PascalCase.tsx
FolderList.tsx

4. The Dependency Rule & Inversion of Control

4. 依赖规则与控制反转

  • Domain is isolated: The
    domain/
    folder MUST NOT import from
    use-cases/
    ,
    infrastructure/
    , or
    presentation/
    .
    • Pragmatic Exception: You MAY import focused libraries like
      zod
      (for complex validations like emails/URLs),
      uuid
      (for UUIDv7 generation), and
      neverthrow
      (for
      Result
      types) inside the Domain. We delegate these tasks because manually parsing emails or generating UUIDv7s is too complex, error-prone, and provides no business value to reinvent.
  • Dependency Inversion: Use Cases MUST depend on Repository Interfaces (from
    domain/interfaces/
    ), not on concrete implementations (like Postgres).
  • No Use Case Chaining: A Use Case MUST NOT instantiate, inject, or call another Use Case.
  • Service Container: Instantiation is centralized in
    src/shared/infrastructure/bootstrap.ts
    (and exported as
    serviceContainer
    ).
    • UI Components and Server Actions MUST NOT instantiate Repositories or Use Cases directly.
    • They MUST import the pre-configured use case instance from
      serviceContainer
      .
    • Example:
      serviceContainer.folders.createFolder.execute(params)
  • 领域层隔离
    domain/
    文件夹禁止从
    use-cases/
    infrastructure/
    presentation/
    导入内容。
    • 实用例外:可在领域层导入特定库,如
      zod
      (用于邮箱/URL等复杂验证)、
      uuid
      (用于生成UUIDv7)和
      neverthrow
      (用于
      Result
      类型)。我们使用这些库是因为手动解析邮箱或生成UUIDv7过于复杂、易出错,且没有业务价值需要重新实现。
  • 依赖反转:用例必须依赖仓库接口(来自
    domain/interfaces/
    ),而非具体实现(如Postgres)。
  • 禁用用例链式调用:一个用例不得实例化、注入或调用另一个用例。
  • 服务容器:实例化集中在
    src/shared/infrastructure/bootstrap.ts
    中(并以
    serviceContainer
    导出)。
    • UI 组件Server Actions不得直接实例化仓库或用例。
    • 必须从
      serviceContainer
      导入预配置的用例实例。
    • 示例:
      serviceContainer.folders.createFolder.execute(params)

5. The
src/app/
Layer (Routing)

5.
src/app/
层(路由

The
src/app/
directory MUST be treated strictly as a Routing and Entry-Point Layer. It MUST contain minimal logic.
  • It MUST wire HTTP requests to your
    presentation/components/
    .
  • app/[locale]/
    : Public-facing pages supporting the custom i18n implementation (e.g., Landing, Login).
  • app/dashboard/
    : Authenticated application views. MUST NOT use the
    [locale]
    segment natively in the URL but still supports i18n through
    LocaleProvider
    .
  • You MUST NOT place domain logic or direct database queries inside
    page.tsx
    or
    layout.tsx
    . You MUST call
    serviceContainer
    use cases or Server Actions instead.
src/app/
目录必须严格作为路由与入口层,仅包含极少逻辑。
  • 必须将HTTP请求关联到
    presentation/components/
  • app/[locale]/
    :支持自定义i18n实现的公开页面(如着陆页、登录页)。
  • app/dashboard/
    :已认证的应用视图。不得在URL中原生使用
    [locale]
    段,但仍通过
    LocaleProvider
    支持i18n。
  • 不得在
    page.tsx
    layout.tsx
    中放置领域逻辑或直接数据库查询,必须调用
    serviceContainer
    用例或Server Actions

6. Shared and Specialized Modules

6. 共享与专用模块

The
src/shared/
Module

src/shared/
模块

The
shared
context MUST contain cross-cutting concerns that apply to the entire application:
  • shared/domain/
    : Base classes, generic utilities (
    Result
    wrappers), and shared branded errors (like
    RepositoryError
    ).
  • shared/infrastructure/
    :
    • bootstrap.ts
      and
      serviceContainer.ts
      (Dependency Injection).
    • load-env.ts
      (Zod environment validation).
    • drizzle-postgres/
      (Database schema and setup).
    • i18n/
      (Custom translation logic).
  • shared/presentation/
    : Reusable Components (e.g., UI library
    shadcn
    ,
    LocaleProvider
    ).
  • shared/test/
    : Reusable testing utilities like
    txTest
    and database seeder helpers.
shared
上下文必须包含适用于整个应用的横切关注点:
  • shared/domain/
    :基类、通用工具(
    Result
    包装器)和共享标记错误(如
    RepositoryError
    )。
  • shared/infrastructure/
    • bootstrap.ts
      serviceContainer.ts
      (依赖注入)。
    • load-env.ts
      (Zod环境变量验证)。
    • drizzle-postgres/
      (数据库Schema与配置)。
    • i18n/
      (自定义翻译逻辑)。
  • shared/presentation/
    :可复用组件(如UI库
    shadcn
    LocaleProvider
    )。
  • shared/test/
    :可复用测试工具,如
    txTest
    和数据库种子数据助手。

The
src/auth/
Module (Auth)

src/auth/
模块(认证

The
auth
context MUST act as a specialized Bounded Context that integrates the Better Auth library. Unlike standard 4-layer modules, it MUST act as an infrastructural bridge:
  • MUST contain
    auth.ts
    (Server Config) and
    auth-client.ts
    (Client Config).
  • MUST provide
    getSession.ts
    for injecting session state into Server Actions and Use Cases.
  • MUST house specific auth UI in
    presentation/components/
    and React Email templates in
    templates/
    .
auth
上下文作为专用Bounded Context,集成Better Auth库。与标准四层模块不同,它必须作为基础设施桥梁:
  • 必须包含
    auth.ts
    (服务端配置)和
    auth-client.ts
    (客户端配置)。
  • 必须提供
    getSession.ts
    ,用于将会话状态注入Server Actions和用例。
  • 必须在
    presentation/components/
    中存放特定认证UI,在
    templates/
    中存放React Email模板。

7. Environment Variables (Zod Validation)

7. 环境变量(Zod验证)

You MUST NOT access
process.env
directly in application code. All environment variables MUST be defined, validated, and exported from a single file:
src/shared/infrastructure/load-env.ts
.
  1. Zod Validation: We MUST use a
    retrieveEnvVar
    helper that takes the environment variable name and a
    z.ZodType
    schema. This MUST throw an immediate, descriptive error on startup if an environment variable is missing or malformed.
  2. Type Coercion: For numbers or booleans, you MUST use Zod's coercion (e.g.,
    z.coerce.number().int().positive()
    ).
  3. Value Object Integration: Variables MAY be mapped directly into a Value Object on load (e.g.,
    AppUrl.from(value)._unsafeUnwrap()
    ) so the rest of the application gets a valid, typed primitive.
  4. Public Variables Prefix: Any exported constant in
    load-env.ts
    that will be exposed to the client/browser MUST have a
    PUBLIC_
    prefix in its TypeScript variable name (e.g.,
    export const PUBLIC_APP_NAME
    ). This visually distinguishes client-safe variables from server-only secrets within the codebase. (The actual environment variable name must still follow Next.js conventions, e.g.,
    NEXT_PUBLIC_APP_NAME
    ).
  5. App Usage: Anywhere in the codebase that needs an environment variable, you MUST import the exported constant directly from
    load-env.ts
    .
不得在应用代码中直接访问
process.env
。所有环境变量必须在单个文件
src/shared/infrastructure/load-env.ts
中定义、验证并导出。
  1. Zod验证:必须使用
    retrieveEnvVar
    助手,传入环境变量名称和
    z.ZodType
    schema。如果环境变量缺失或格式错误,必须在启动时立即抛出描述性错误。
  2. 类型转换:对于数字或布尔值,必须使用Zod的转换功能(如
    z.coerce.number().int().positive()
    )。
  3. 值对象集成:变量可在加载时直接映射到值对象(如
    AppUrl.from(value)._unsafeUnwrap()
    ),以便应用其他部分获取有效的类型化基础值。
  4. 公开变量前缀
    load-env.ts
    中任何将暴露给客户端/浏览器的导出常量,其TypeScript变量名必须带有
    PUBLIC_
    前缀(如
    export const PUBLIC_APP_NAME
    )。这在代码库中直观区分客户端安全变量与服务端专属密钥。(实际环境变量名称仍需遵循Next.js规范,如
    NEXT_PUBLIC_APP_NAME
    )。
  5. 应用使用:代码库中任何需要环境变量的地方,必须直接从
    load-env.ts
    导入导出的常量。

Example from
load-env.ts
:

load-env.ts
示例:

typescript
import { z } from "zod";
import { AppUrl } from "@/shared/domain/value-objects/appUrl";

// Base helper
function retrieveEnvVar<T>(params: { name: string; schema: z.ZodType<T> }): T {
  const value = process.env[params.name];
  const parsed = params.schema.safeParse(value);
  if (parsed.success) return parsed.data;
  throw new TypeError(`Environment variable ${params.name} is invalid...`);
}

// 1. Standard string validation
export const PUBLIC_APP_NAME = retrieveEnvVar({
  name: "NEXT_PUBLIC_APP_NAME",
  schema: z.string().min(3),
});

// 2. Coercion (e.g., port to number)
export const POSTGRES_PORT = retrieveEnvVar({
  name: "POSTGRES_PORT",
  schema: z.coerce.number().int().positive(),
});

// 3. Wrapping in a Value Object
export const PUBLIC_APP_BASE_URL = AppUrl.from(
  retrieveEnvVar({
    name: "NEXT_PUBLIC_APP_BASE_URL",
    schema: z.url(),
  }),
)._unsafeUnwrap({ withStackTrace: true });
typescript
import { z } from "zod";
import { AppUrl } from "@/shared/domain/value-objects/appUrl";

// 基础助手函数
function retrieveEnvVar<T>(params: { name: string; schema: z.ZodType<T> }): T {
  const value = process.env[params.name];
  const parsed = params.schema.safeParse(value);
  if (parsed.success) return parsed.data;
  throw new TypeError(`Environment variable ${params.name} is invalid...`);
}

// 1. 标准字符串验证
export const PUBLIC_APP_NAME = retrieveEnvVar({
  name: "NEXT_PUBLIC_APP_NAME",
  schema: z.string().min(3),
});

// 2. 类型转换(如端口转为数字)
export const POSTGRES_PORT = retrieveEnvVar({
  name: "POSTGRES_PORT",
  schema: z.coerce.number().int().positive(),
});

// 3. 包装为值对象
export const PUBLIC_APP_BASE_URL = AppUrl.from(
  retrieveEnvVar({
    name: "NEXT_PUBLIC_APP_BASE_URL",
    schema: z.url(),
  }),
)._unsafeUnwrap({ withStackTrace: true });

8. Testing Strategy & Co-location

8. 测试策略与同目录存放

Tests MUST live right next to the code they verify.
  • *.test.ts
    : Fast, isolated Unit Tests logic (Domain Models, Use Cases). MUST be handled by Vitest's unit worker.
  • *.integration.test.ts
    : Slower tests requiring infrastructure (Postgres/Testcontainers). MUST be placed inside
    infrastructure/
    directories and run via the
    txTest
    helper to rollback changes.
  • *.ui.test.ts
    : For testing components in
    presentation/
    .
测试必须与被验证的代码放在同一目录下。
  • *.test.ts
    :快速、隔离的单元测试逻辑(领域模型、用例),必须由Vitest的单元工作器处理。
  • *.integration.test.ts
    :需要基础设施的慢速测试(Postgres/Testcontainers),必须放在
    infrastructure/
    目录下,通过
    txTest
    助手运行以回滚更改。
  • *.ui.test.ts
    :用于测试
    presentation/
    中的组件。

9. Control Flow Summary (A Complete Request)

9. 控制流程总结(完整请求)

The control flow MUST be as follows:
  1. User interacts with a Client Component (
    presentation/components/PascalCase.tsx
    ).
  2. Component MUST call a Server Action (
    presentation/actions/camelCaseAction.ts
    ).
  3. Server Action MUST validate raw inputs (via
    Zod
    ), authenticate the user, and call a Use Case via
    serviceContainer
    .
  4. The Use Case (
    use-cases/camelCase.ts
    ) MUST orchestrate logic:
    • MUST use
      static from()
      on Value Objects to create valid domain primitives.
    • MUST call Repository Interfaces (
      domain/interfaces/
      ) to fetch/save data.
    • MUST return a
      Result<Data, DomainError>
      (
      neverthrow
      ).
  5. Server Action MUST receive the
    Result
    . If error, it MUST use
    assertNever()
    to exhaustively map it to a UI-friendly error response string. If success, it MUST return data mapped via
    .toBranded()
    . Layer boundaries MUST be maintained.
控制流程必须如下:
  1. 用户交互:用户与客户端组件
    presentation/components/PascalCase.tsx
    )交互。
  2. 组件必须调用**Server Action**(
    presentation/actions/camelCaseAction.ts
    )。
  3. Server Action必须通过
    Zod
    验证原始输入,认证用户,并通过
    serviceContainer
    调用**用例**。
  4. 用例
    use-cases/camelCase.ts
    )必须编排逻辑:
    • 必须使用**值对象**的
      static from()
      方法创建有效的领域基础值。
    • 必须调用**仓库接口**(
      domain/interfaces/
      )来获取/保存数据。
    • 必须返回
      Result<Data, DomainError>
      neverthrow
      )。
  5. Server Action必须接收
    Result
    。如果是错误,必须使用
    assertNever()
    将其全面映射为UI友好的错误响应字符串。如果是成功,必须通过
    .toBranded()
    返回映射后的数据。必须保持层边界。