clean-architecture-ts
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseClean Architecture for Remix/TypeScript Apps
适用于Remix/TypeScript应用的整洁架构
As Remix apps grow, and functions can become bloated "God Functions". This skill emphasizes separation of concerns.
loaderaction随着Remix应用规模增长,和函数可能会变成臃肿的“上帝函数”。本内容重点讲解关注点分离的实践。
loaderaction1. 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 to manage dependencies, especially for testing (mocking Repositories).
tsyringetypescript
// app/services/order.service.ts
@injectable()
export class OrderService {
constructor(
@inject(OrderRepository) private orderRepo: OrderRepository,
@inject(ShopifyClient) private shopify: ShopifyClient
) {}
}对于复杂应用,使用如之类的容器来管理依赖,尤其便于测试(对仓储层进行mock)。
tsyringetypescript
// 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 / to catch these errors and return appropriate HTTP status codes.
loaderaction创建自定义错误类,区分“错误请求”(用户侧错误)和“服务器错误”(系统侧错误)。
typescript
// app/errors/index.ts
export class BusinessError extends Error {
public code = 422;
}
export class NotFoundError extends Error {
public code = 404;
}重构你的/来捕获这些错误,并返回合适的HTTP状态码。
loaderaction