Can Clean Architecture Transform Your Frontend? A Practical React/TypeScript Guide
This article explains how to apply Clean Architecture principles to a frontend React/TypeScript project, covering the three‑layer structure, dependency rules, domain modeling, use‑case design, adapter implementation, code organization, trade‑offs, and practical tips for building a cookie‑store example.
Clean Architecture Overview
Clean Architecture separates a system into three concentric layers – Domain , Application , and Adapters . Outer layers may depend on inner ones, but inner layers must never depend on outer layers.
Domain Layer
The domain contains the core business entities and pure type definitions. In the cookie‑store example the entities are User, Product, Cart, and Order. Shared‑kernel type aliases keep the domain independent of external libraries.
export type User = {
id: UniqueId;
name: UserName;
email: Email;
preferences: Ingredient[];
allergies: Ingredient[];
};
export type Product = {
id: UniqueId;
title: ProductTitle;
price: PriceCents;
toppings: Ingredient[];
};
export type Cart = {
products: Product[];
};
export type Order = {
user: UniqueId;
cart: Cart;
created: DateTimeString;
status: OrderStatus;
total: PriceCents;
};Application Layer
This layer defines use‑cases (application scenarios) and the ports (interfaces) that the core uses to communicate with the outside world.
export type OrderProducts = (user: User, cart: Cart) => Promise<void>;Key ports: PaymentService –
tryPay(amount: PriceCents): Promise<boolean> NotificationService–
notify(message: string): void OrdersStorageService–
orders: Order[]; updateOrders(orders: Order[]): voidAdapter Layer
Adapters translate external APIs into the shapes required by the ports. The example provides a fake payment API, an alert‑based notifier, and a React‑context storage adapter.
export function usePayment(): PaymentService {
return {
tryPay: (amount) => fakeApi(true)
};
}
export function useNotifier(): NotificationService {
return {
notify: (msg) => window.alert(msg)
};
}
export function fakeApi<TResponse>(response: TResponse): Promise<TResponse> {
return new Promise(res => setTimeout(() => res(response), 450));
}Dependency Rule
Domain must not depend on any other layer.
Application may depend on Domain.
Adapters may depend on any layer.
Core Domain Functions
Utility functions operate purely on domain data:
export function hasAllergy(user: User, ingredient: Ingredient): boolean {
return user.allergies.includes(ingredient);
}
export function hasPreference(user: User, ingredient: Ingredient): boolean {
return user.preferences.includes(ingredient);
}
export function addProduct(cart: Cart, product: Product): Cart {
return { ...cart, products: [...cart.products, product] };
}
export function contains(cart: Cart, product: Product): boolean {
return cart.products.some(p => p.id === product.id);
}
export function totalPrice(products: Product[]): PriceCents {
return products.reduce((t, { price }) => t + price, 0);
}
export function createOrder(user: User, cart: Cart): Order {
return {
user: user.id,
cart,
created: new Date().toISOString(),
status: "new",
total: totalPrice(cart.products)
};
}Checkout Use‑Case Implementation
The checkout scenario validates data, creates an order, attempts payment, notifies on failure, and persists the order on success.
async function orderProducts(user: User, cart: Cart) {
const order = createOrder(user, cart);
const paid = await payment.tryPay(order.total);
if (!paid) return notifier.notify("Payment failed");
orderStorage.updateOrders([...orderStorage.orders, order]);
cartStorage.emptyCart();
}In a real project the use‑case would receive its dependencies via injection rather than pulling them from React hooks, making the function pure and easily testable.
Shared Kernel Types
Basic type aliases that do not introduce external dependencies:
type Email = string;
type UniqueId = string;
type DateTimeString = string;
type PriceCents = number;Design Improvements
Rich price model : replace PriceCents with an object { value: number, currency: string } to capture currency information.
Feature‑based folder layout : organize code by feature (e.g., cart/, order/) instead of strict layer folders.
Shared utilities : extract helpers such as currentDatetime() to avoid hidden side‑effects.
Explicit dependency injection : pass required services to use‑cases via parameters or a DI container, keeping use‑cases pure.
Benefits of Clean Architecture
Domain independence : business logic is isolated and highly testable.
Replaceable third‑party services : adapters allow swapping implementations without touching core code.
High cohesion, low coupling : each layer has a single responsibility.
Costs and Trade‑offs
Time overhead : additional design and implementation effort.
Potential over‑engineering : small projects may become unnecessarily complex.
Bundle size increase : extra files add to the final JavaScript payload.
Conclusion
Applying Clean Architecture to a frontend project yields clear separation of concerns, easier testing, and flexibility to replace external services. While it introduces upfront design work and a larger bundle, the architectural benefits become evident as the codebase grows and requirements evolve.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
