
Kết quả
Phạm vi dự án
NovaShop khởi đầu là một storefront nhỏ, và tôi mở rộng dần cho tới khi nó vận hành được như một cửa hàng thật: tiền đi qua Stripe, dữ liệu sản phẩm và đơn hàng nằm trong PostgreSQL, người vận hành có app quản trị riêng cho công việc hằng ngày. Tôi đầu tư vào những phần này vì thanh toán, đăng nhập và nghiệp vụ vận hành là nơi quyết định kỹ thuật có hậu quả thật: một webhook handler viết sai không chỉ là lỗi giao diện, mà có thể khiến khách bị trừ tiền hai lần.
Cả ba app nằm trong cùng một pnpm workspace. Một tính năng như xử lý đơn hàng thường ảnh hưởng tới cả storefront, API lẫn admin; monorepo cho phép review những thay đổi đó trong cùng một nhánh, trong khi mỗi app vẫn build và deploy độc lập.
Kiến trúc
Storefront và dashboard quản trị đều gọi thẳng vào API NestJS, không có lớp proxy trung gian. Chỉ những route thật sự cần chạy cùng storefront mới nằm trong Next.js: NextAuth, endpoint AI chat, và phần checkout, webhook của Stripe. Business rule, dữ liệu và trạng thái thanh toán đều do API quản lý. Ở storefront, mọi request cần đăng nhập đi qua một helper authFetch duy nhất, nơi access token được gắn vào và tự refresh khi hết hạn.
API cung cấp REST kèm tài liệu Swagger, song song với GraphQL qua Apollo. Giỏ hàng và review dùng GraphQL vì client cần tự chọn cấu trúc dữ liệu trả về; các luồng còn lại dùng REST.
- Storefront: Next.js 15.5 App Router, React 19, Tailwind CSS, deploy trên Vercel.
- API: NestJS 11, TypeORM trên PostgreSQL (Supabase), chạy cả REST lẫn GraphQL.
- Admin: React 19, Vite, TanStack Query và upload ảnh qua Cloudinary.
- Workspace: ba app được quản lý bằng pnpm và kiểm tra bằng GitHub Actions CI.
Phần storefront
App Next.js phụ trách toàn bộ hành trình mua hàng: tìm kiếm, lọc sản phẩm, trang chi tiết, giỏ hàng, wishlist, checkout và lịch sử đơn. Các trang catalog render bằng React Server Components nên nội dung sản phẩm có sẵn trong HTML ban đầu thay vì phải chờ fetch. Checkout không bắt buộc tài khoản: khách vãng lai mua trực tiếp một sản phẩm và Stripe thu địa chỉ giao hàng ngay khi thanh toán, còn khách đã đăng nhập thanh toán cả giỏ với địa chỉ đã lưu.
Storefront tự sinh sitemap, robots.txt, JSON-LD, Open Graph và Twitter metadata. Trong một lần đo Lighthouse trên desktop, app đạt 98 điểm performance, 96 accessibility, 100 best practices và 100 SEO; LCP 1,1 giây, CLS bằng 0. Google Analytics và Sentry được tích hợp sẵn, nên lưu lượng truy cập và lỗi production đều theo dõi được mà không cần vào log server.
Phần API
API NestJS chia module theo domain: auth, người dùng, sản phẩm, giỏ hàng, wishlist, đơn hàng, địa chỉ, review, notification, analytics và poster cho storefront. Lọc và sắp xếp chạy ngay trong PostgreSQL thay vì kéo dữ liệu về xử lý trong bộ nhớ. TypeORM synchronize được tắt, mọi thay đổi schema đều phải áp dụng một cách tường minh.
Việc tạo đơn chạy trọn trong một database transaction. Tồn kho được trừ bằng câu update có điều kiện, chỉ thành công khi còn đủ hàng, nên hai người cùng mua món cuối cùng thì chỉ một người mua được. Mỗi đơn cũng lưu một bản snapshot địa chỉ giao hàng riêng, vì vậy người dùng sửa địa chỉ sau này không làm thay đổi thông tin của các đơn đã đặt.
Dashboard quản trị
Tài khoản admin và staff dùng một app React và Vite riêng để quản lý sản phẩm, đơn hàng, khách hàng, poster và số liệu bán hàng. TanStack Query quản lý server state; ảnh sản phẩm và poster được upload qua Cloudinary.
Thanh toán
Stripe Checkout được dùng cho cả mua ngay lẫn thanh toán nguyên giỏ, và không đơn nào được xác nhận từ lượt redirect về trình duyệt. Webhook của Stripe đi vào một route Next.js để xác minh chữ ký sự kiện; route này gọi tiếp một endpoint xác nhận nội bộ trên API, được bảo vệ bằng một shared secret riêng. Như vậy giữa internet và một đơn hàng được xác nhận luôn có hai lớp kiểm tra độc lập.
Bước xác nhận là idempotent: handler kiểm tra Stripe session id ngay trong transaction, nên webhook gửi lại bao nhiêu lần cũng chỉ trả về đúng đơn đã tạo. Đơn của khách vãng lai lưu email người mua; nếu email trùng với một tài khoản đã đăng ký, đơn được gắn vào tài khoản đó và hiện trong lịch sử mua hàng. Sau khi đơn được ghi nhận, hệ thống gửi email xác nhận cho người mua.
Đăng nhập và bảo mật
Người dùng đăng nhập bằng email và mật khẩu hoặc Google. NestJS phát hành access token và refresh token riêng biệt; storefront lưu cả hai trong cookie httpOnly thông qua NextAuth v5. Refresh token được hash bằng bcrypt trong database và thay mới sau mỗi lần dùng, nên token có lộ ra ngoài cũng không dùng lại được. API còn có rate limiting, role guard và ownership check để một tài khoản không truy cập được giỏ hàng, đơn hàng hay địa chỉ của tài khoản khác.
Trợ lý mua sắm AI
Trợ lý mua sắm dùng xAI Grok qua Vercel AI SDK. Công cụ searchProducts truy vấn catalog hiện tại theo từ khóa, danh mục và giá, nên gợi ý sản phẩm dựa trên dữ liệu đang có trong cửa hàng.
Caching và publish
Catalog công khai dùng ISR, request theo tài khoản không dùng shared cache, còn mutation sẽ invalidate dữ liệu theo tag. Cache helper hỗ trợ các API tương ứng của Next.js 15 và 16 để hành vi không đổi trong quá trình nâng framework.
Kiểm thử
Mỗi app có bộ test riêng: Jest cho API, Vitest cho storefront và admin. Test tập trung vào những chỗ mà regression gây thiệt hại thật về tiền hoặc dữ liệu: chuyển trạng thái đơn hàng và hoàn kho khi hủy đơn, các schema validate form ở admin, và phần parse bộ lọc catalog điều khiển URL của storefront. Mỗi lần push, GitHub Actions chạy đầy đủ các bước: lint, ba bộ unit test, một lượt Cypress end-to-end tự khởi động storefront và đi qua các luồng điều hướng catalog, tìm kiếm, bộ lọc, giỏ hàng, phân tích tĩnh bằng SonarQube để phát hiện vấn đề về chất lượng và bảo mật mã nguồn, rồi mới build production.
Giới hạn và hướng phát triển
Có hai đánh đổi đáng nói. Webhook của Stripe hiện đi vào storefront trước rồi mới chuyển tiếp sang API; luồng này được xác minh ở cả hai chặng, nhưng nếu làm lại tôi sẽ trỏ Stripe thẳng vào một endpoint NestJS cho gọn. Email xác nhận cũng đang gửi ngay trong luồng tạo đơn, ổn ở quy mô hiện tại nhưng khi lượng đơn lớn thì nên chuyển sang queue. Cả hai đều là lựa chọn có cân nhắc cho hệ thống cỡ này, và là những thứ tôi sẽ xem lại đầu tiên nếu dự án lớn lên.