clean-architecture-ts

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Clean Architecture for Remix/TypeScript Apps

适用于Remix/TypeScript应用的整洁架构

As Remix apps grow,
loader
and
action
functions can become bloated "God Functions". This skill emphasizes separation of concerns.
随着Remix应用规模增长,
loader
action
函数可能会变成臃肿的“上帝函数”。本内容重点讲解关注点分离的实践。

1. The Layers

1. 架构分层

A. The Web Layer (Loaders/Actions)

A. Web层(Loaders/Actions)

Responsibility: Parsing requests, input validation (Zod), and returning Responses (JSON/Redirect). Rule: NO business logic here. Only orchestration.
typescript
// app/routes/app.products.update.ts
export const action = async ({ request }: ActionFunctionArgs) => {
  const { shop } = await authenticate.admin(request);
  const formData = await request.formData();
  
  // 1. Validate Input
  const input = validateProductUpdate(formData);

  // 2. Call Service
  const updatedProduct = await ProductService.updateProduct(shop, input);

  // 3. Return Response
  return json({ product: updatedProduct });
};
责任:解析请求、输入验证(Zod)以及返回响应(JSON/重定向)。 规则:此处不要编写业务逻辑,仅做流程编排。
typescript
// app/routes/app.products.update.ts
export const action = async ({ request }: ActionFunctionArgs) => {
  const { shop } = await authenticate.admin(request);
  const formData = await request.formData();
  
  // 1. Validate Input
  const input = validateProductUpdate(formData);

  // 2. Call Service
  const updatedProduct = await ProductService.updateProduct(shop, input);

  // 3. Return Response
  return json({ product: updatedProduct });
};

B. The Service Layer (Business Logic)

B. 服务层(业务逻辑)

Responsibility: The "What". Rules, calculations, error handling, complex flows. Rule: Framework agnostic. Should not know about "Request" or "Response" objects.
typescript
// app/services/product.service.ts
export class ProductService {
  static async updateProduct(shop: string, input: ProductUpdateInput) {
    // Business Rule: Can't update archived products
    const existing = await ProductRepository.findByShopAndId(shop, input.id);
    if (existing.status === 'ARCHIVED') {
      throw new BusinessError("Cannot update archived product");
    }

    // Business Logic
    const result = await ProductRepository.save({
      ...existing,
      ...input,
      updatedAt: new Date()
    });

    return result;
  }
}
责任:负责处理“做什么”的逻辑,包括规则、计算、错误处理、复杂流程。 规则:与框架无关,不应该感知到“Request”或“Response”对象的存在。
typescript
// app/services/product.service.ts
export class ProductService {
  static async updateProduct(shop: string, input: ProductUpdateInput) {
    // Business Rule: Can't update archived products
    const existing = await ProductRepository.findByShopAndId(shop, input.id);
    if (existing.status === 'ARCHIVED') {
      throw new BusinessError("Cannot update archived product");
    }

    // Business Logic
    const result = await ProductRepository.save({
      ...existing,
      ...input,
      updatedAt: new Date()
    });

    return result;
  }
}

C. The Repository Layer (Data Access)

C. 仓储层(数据访问)

Responsibility: The "How". interaction with Database (Prisma), APIs (Shopify Admin), or File System. Rule: Only this layer touches the DB/API.
typescript
// app/repositories/product.repository.ts
export class ProductRepository {
  static async findByShopAndId(shop: string, id: string) {
    return prisma.product.findFirstOrThrow({
      where: { shop, id: BigInt(id) }
    });
  }
}
责任:负责处理“怎么做”的逻辑,与数据库(Prisma)、API(Shopify Admin)或文件系统交互。 规则:只有这一层可以操作数据库/调用外部API。
typescript
// app/repositories/product.repository.ts
export class ProductRepository {
  static async findByShopAndId(shop: string, id: string) {
    return prisma.product.findFirstOrThrow({
      where: { shop, id: BigInt(id) }
    });
  }
}

2. Directory Structure

2. 目录结构

app/
  routes/         # Web Layer
  services/       # Business Logic
  repositories/   # Data Access (DB/API)
  models/         # Domain Types / Interfaces
  utils/          # Pure functions (math, string manipulation)
app/
  routes/         # Web层
  services/       # 业务逻辑
  repositories/   # 数据访问(DB/API)
  models/         # 领域类型 / 接口
  utils/          # 纯函数(数学计算、字符串处理)

3. Dependency Injection (Optional but Recommended)

3. 依赖注入(可选但推荐)

For complex apps, use a container like
tsyringe
to manage dependencies, especially for testing (mocking Repositories).
typescript
// app/services/order.service.ts
@injectable()
export class OrderService {
  constructor(
    @inject(OrderRepository) private orderRepo: OrderRepository,
    @inject(ShopifyClient) private shopify: ShopifyClient
  ) {}
}
对于复杂应用,使用如
tsyringe
之类的容器来管理依赖,尤其便于测试(对仓储层进行mock)。
typescript
// app/services/order.service.ts
@injectable()
export class OrderService {
  constructor(
    @inject(OrderRepository) private orderRepo: OrderRepository,
    @inject(ShopifyClient) private shopify: ShopifyClient
  ) {}
}

4. Error Handling

4. 错误处理

Create custom Error classes to differentiate between "Bad Request" (User error) and "Server Error" (System error).
typescript
// app/errors/index.ts
export class BusinessError extends Error {
  public code = 422;
}

export class NotFoundError extends Error {
  public code = 404;
}
Refactor your
loader
/
action
to catch these errors and return appropriate HTTP status codes.
创建自定义错误类,区分“错误请求”(用户侧错误)和“服务器错误”(系统侧错误)。
typescript
// app/errors/index.ts
export class BusinessError extends Error {
  public code = 422;
}

export class NotFoundError extends Error {
  public code = 404;
}
重构你的
loader
/
action
来捕获这些错误,并返回合适的HTTP状态码。