Ngoài UI, một dự án E-commerce có rất nhiều logic nghiệp vụ lặp lại. Nếu copy-paste code từ Admin sang Store, bạn đang tạo ra nợ kỹ thuật (Technical Debt).
1. Cấu trúc packages/utils
Code:
packages/utils/
├── src/
│ ├── currency.ts
│ ├── date.ts
│ └── index.ts
├── package.json
└── tsconfig.jsonFormat tiền tệ (Currency)
Tiền Việt Nam có quy tắc riêng. Đừng hardcode vnd ở khắp nơi.
typescript:
// packages/utils/src/currency.ts
export const formatPrice = (price: number | string) => {
const numericPrice = typeof price === "string" ? parseFloat(price) : price;
return new Intl.NumberFormat("vi-VN", {
style: "currency",
currency: "VND",
maximumFractionDigits: 0,
}).format(numericPrice);
};Date Parsing (date-fns wrapper)
Thống nhất format ngày tháng dd/MM/yyyy toàn hệ thống.
typescript:
// packages/utils/src/date.ts
import { format, parseISO } from "date-fns";
import { vi } from "date-fns/locale";
export const formatDate = (date: string | Date) => {
const d = typeof date === "string" ? parseISO(date) : date;
return format(d, "dd 'tháng' MM, yyyy", { locale: vi });
};2. Shared Zod Schemas (@repo/schema)
Validation logic là thứ quan trọng nhất cần share. Backend dùng nó để validate API body, Frontend dùng nó để validate Form.
typescript:
// packages/schema/src/auth.ts
import { z } from "zod";
export const loginSchema = z.object({
email: z.string().email("Email không hợp lệ"),
password: z.string().min(6, "Mật khẩu tối thiểu 6 ký tự"),
});
export const registerSchema = loginSchema.extend({
fullName: z.string().min(2, "Tên phải dài hơn 2 ký tự"),
});
export type LoginInput = z.infer<typeof loginSchema>;Sử dụng ở Frontend (React Hook Form)
typescript:
import { loginSchema } from "@repo/schema";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
const form = useForm({
resolver: zodResolver(loginSchema)
});Sử dụng ở Backend (Server Action)
typescript:
import { loginSchema } from "@repo/schema";
export async function loginAction(formData: FormData) {
const raw = Object.fromEntries(formData);
const result = loginSchema.safeParse(raw);
if (!result.success) {
return { error: "Dữ liệu không hợp lệ" };
}
}3. Custom Hooks (@repo/hooks)
Chia sẻ logic React.
typescript:
// packages/hooks/use-debounce.ts
import { useEffect, useState } from "react";
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}Tổng kết
Việc tách logic ra khỏi UI Component giúp:
- Dễ test: Viết Unit Test cho
formatPricedễ hơn test cả cái componentProductCard. - Consistency: 1 tỷ đồng luôn hiển thị là
1.000.000.000 ₫chứ không phải1000trhay1 tỷ. - Type Safety: Schema được đồng bộ giữa FE và BE.