clean-architecture-ts
Clean Architecture for Remix/TypeScript Apps
As Remix apps grow, loader and action functions can become bloated "God Functions". This skill emphasizes separation of concerns.
1. The Layers
A. The Web Layer (Loaders/Actions)
Responsibility: Parsing requests, input validation (Zod), and returning Responses (JSON/Redirect). Rule: NO business logic here. Only orchestration.
// 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)
Responsibility: The "What". Rules, calculations, error handling, complex flows. Rule: Framework agnostic. Should not know about "Request" or "Response" objects.
// 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)
Responsibility: The "How". interaction with Database (Prisma), APIs (Shopify Admin), or File System. Rule: Only this layer touches the DB/API.
// 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
app/
routes/ # Web Layer
services/ # Business Logic
repositories/ # Data Access (DB/API)
models/ # Domain Types / Interfaces
utils/ # Pure functions (math, string manipulation)
3. Dependency Injection (Optional but Recommended)
For complex apps, use a container like tsyringe to manage dependencies, especially for testing (mocking Repositories).
// app/services/order.service.ts
@injectable()
export class OrderService {
constructor(
@inject(OrderRepository) private orderRepo: OrderRepository,
@inject(ShopifyClient) private shopify: ShopifyClient
) {}
}
4. Error Handling
Create custom Error classes to differentiate between "Bad Request" (User error) and "Server Error" (System error).
// 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.
More from toilahuongg/google-antigravity-kit
shopify-liquid
Guide for using the Liquid template language within Shopify Theme App Extensions and Themes. Use this skill when building App Embed Blocks, App Blocks, or modifying Shopify Themes.
50shopify-polaris-design
Design and implement Shopify Admin interfaces using the Polaris Design System. Use this skill when building Shopify Apps, Admin extensions, or any interface that needs to feel native to Shopify.
47docusaurus-generator
Generate end-user documentation site using Docusaurus 3.x from the current project. Use this skill when the user asks to create documentation, generate docs, build a docs site, or set up Docusaurus for their project. Supports analyzing project structure, generating markdown docs, configuring Docusaurus, and creating user guides.
31shopify-remix-template
Guide for developing Shopify apps using the official Shopify Remix Template. Covers structure, authentication, API usage, and deployment.
19remotion-best-practices
Best practices for Remotion - Video creation in React
18remixjs-best-practices
Best practices for Remix (2025-2026 Edition), focusing on React Router v7 migration, server-first data patterns, and error handling.
15