Ngày nay, thật dễ để nghĩ rằng Django đã lỗi thời. Những công cụ và stack hợp thời như Next.js, Supabase, Astro, và T3 Stack đang xuất hiện khắp nơi. Chúng trông nhanh, hiện đại và đi kèm rất nhiều tính năng tích hợp sẵn. Bạn có thể triển khai một ứng dụng full-stack chỉ trong vài giờ và bắt đầu nhanh chóng — đúng với điều quan trọng nhất đối với một nhà xây dựng: tạo ra giá trị.
Tuy nhiên, khi tôi cần xây dựng một backend SaaS thực thụ — đòi hỏi sự kiểm soát, cấu trúc rõ ràng và khả năng bảo trì lâu dài — tôi vẫn chọn Django là lựa chọn hàng đầu.
Nó có thể không phải là xu hướng, nhưng lại đáng tin cậy. Và khi kết hợp với những công cụ phù hợp, Django vô cùng mạnh mẽ.
Những điều các công cụ hiện đại làm đúng
Phải thừa nhận rằng Next.js và các công cụ tương tự rất ấn tượng. Bạn có frontend và backend cùng chỗ, triển khai lên Vercel chỉ bằng một cú click, và các thư viện như Prisma hay Auth.js giúp bạn thiết lập rất nhanh chóng.
Nếu bạn đang xây dựng:
- Một landing page
- Một prototype
- Một MVP SaaS có độ phức tạp thấp
...thì stack JavaScript là quá tuyệt. Tôi cũng từng dùng và nó giúp tôi triển khai nhanh chóng. Nhưng tôi không thích sự hỗn loạn xuất hiện khi ứng dụng bắt đầu phát triển. Và đó là điểm mà tôi cho rằng Django làm tốt hơn.
Điều Django vẫn làm tốt hơn
Nếu Django và các công cụ hiện đại được ví như trong câu chuyện “Rùa và Thỏ”, thì tôi nghĩ Django chính là chú rùa. Khi sản phẩm của bạn bắt đầu cần:
- Mô hình dữ liệu phức tạp hơn
- API thực thụ
- Hệ thống phân quyền tinh vi
- Kiểm soát lâu dài với backend
...thì các stack frontend-first hiện đại bắt đầu trở nên hạn chế.
Django thì ngược lại — nó buộc bạn phải rõ ràng. Bạn phải định nghĩa model, serializer, view, và route một cách tường minh. Nghe có vẻ nhiều việc hơn ban đầu, nhưng cấu trúc này thực sự giúp ích. Logic được giữ sạch sẽ, dễ debug, và khi có người mới tham gia dự án, họ không phải đoán xem logic nằm ở đâu.
Chưa kể, với các công cụ AI như Copilot, GPT, hay Cursor, bạn có thể tạo hầu hết đoạn code "boilerplate" trong vài giây:
“Viết một ViewSet trong Django REST Framework cho model này và đăng ký nó trong router.”
Điều từng là lặp đi lặp lại nay chỉ cần một dòng prompt. Cuối cùng, chú rùa đã trở thành ninja rùa.
Tuy nhiên, khi xây dựng API SaaS đầu tiên với Django, tôi gặp một vấn đề: phân phối API Key cho người dùng.
API Key với Django là một cơn ác mộng
Và không, bạn không thể dùng JWT cho API Key. Đó là quan điểm của tôi. Một sai lầm phổ biến trong API SaaS là cách các dev xử lý authentication. Nhiều tổ chức sử dụng JWT cho tất cả mọi thứ — ứng dụng, người dùng, máy chủ, dịch vụ nội bộ và đối tác.
Nhưng JWT được sinh ra để xác thực người dùng, không phải dùng làm API Key cho bên ngoài. Nghe hơi trừu tượng, nên tôi sẽ giải thích:
1. JWT là loại token tạm thời
JWT thường được phát sau khi xác thực thành công (login) và:
- Chứa thông tin mã hoá như ID người dùng, scopes...
- Có thời hạn ngắn (thường 15 phút – 1 giờ)
- Được refresh thường xuyên bằng refresh token
Bạn không thể thu hồi JWT một khi đã phát ra, trừ khi duy trì danh sách đen (blacklist) hoặc xoay vòng secret liên tục. Tính tự chứa của nó giúp xác thực nhanh, nhưng đồng nghĩa với việc một khi token được phát ra, nó sẽ hợp lệ cho đến khi hết hạn.
Mà API Key thì ngược lại — được phát một lần, dùng trong nhiều tuần/tháng. Để hỗ trợ thu hồi JWT, bạn phải lưu nó trong database — đi ngược lại với cách JWT được thiết kế.
2. JWT dẫn đến việc "nhồi nhét thông tin"
Tôi từng thấy các dev nhồi quá nhiều thứ vào JWT khi dùng làm API Key:
{ "sub": "org_abc", "role": "super", "scopes": ["read", "write"], "feature_flags": ["beta_search"], "tenant_id": "xyz"
}
Khi API Key không chỉ dùng cho quyền truy cập mà còn nhồi cả logic vào token, nó trở nên khó kiểm soát và nguy hiểm.
API Key nên là:
- Dạng opaque (không chứa thông tin rõ ràng)
- Đơn giản
- Gắn với metadata trong ứng dụng (không phải logic phía client)
3. Không có chiến lược xoay vòng (rotation)
Nếu bạn thay đổi secret trong hệ thống JWT, tất cả token cũ đều vô hiệu hóa. Client sẽ bị lỗi ngay lập tức. Với JWT, bạn không thể hỗ trợ nhiều secret đồng thời một cách tự nhiên. Bạn phải:
- Chấp nhận downtime và phát lại token, hoặc
- Xây hệ thống quản lý key ID tùy chỉnh
Điều này làm phức tạp hóa việc bảo mật API Key. Trong khi đó, hệ thống API Key tốt cần có:
- Hỗ trợ nhiều secret cùng lúc
- Cho phép xoay vòng mượt mà
- Tùy chỉnh scope hoặc rate limit
JWT không làm được điều này một cách dễ dàng.
Quay lại với Django, khi tôi xây dựng hệ thống thanh toán đầu tiên, tôi cần API Key authorization. May mắn thay, có gói: Django REST Framework API Key
. Rất tiện, dễ triển khai, và hoạt động ổn.
Nhưng... tôi gặp vấn đề về hiệu năng.
Gói này tạo ra API Key, rồi lưu hash của key bằng framework mã hoá mật khẩu của Django — vốn được thiết kế an toàn chứ không nhanh. Khi có request, gói này phải hash key được gửi lên và tìm trong DB — chậm khi có nhiều permission logic chồng lên nhau.
Tôi đã tối ưu bằng cách chuyển sang dùng Argon2. Thời gian giảm từ 5 giây xuống còn khoảng 2.5 giây. Nhưng vẫn chưa đủ tốt.
Thêm nữa, gói này không hỗ trợ xoay vòng, và phải tự viết logging, tracking. Nó cũng không hoàn toàn theo pattern xác thực của Django, vốn dựa vào request.user
.
Vậy là tôi quyết định tự xây gói: drf-simple-api-key
Tôi dùng Fernet (trong thư viện cryptography
) để mã hóa API Key. Fernet hỗ trợ xoay vòng key một cách tự nhiên. Bạn có thể mã hóa bằng key hiện tại, rồi giải mã bằng key cũ nếu cần — hoàn hảo để xoay vòng mà không gây gián đoạn.
Tôi bắt đầu nhỏ, tập trung cải thiện hiệu năng và theo đúng mô hình permission của Django. Sau đó, tôi thêm:
- Xác thực bằng API Key
- Hỗ trợ xoay vòng
- Theo dõi usage và thống kê
Ví dụ về cách dùng MultiFernet
:
from cryptography.fernet import MultiFernet, Fernet # Your current and previous keys
current_key = Fernet(b'CURRENT_SECRET_KEY')
previous_key = Fernet(b'OLD_SECRET_KEY') # Use MultiFernet for seamless decryption
f = MultiFernet([current_key, previous_key]) # Encrypt new API keys using the current key
encrypted = f.encrypt(b"api-key:my-user-123") # Decrypt any key (old or new)
decrypted = f.decrypt(encrypted)
Tôi còn thêm setting để khởi động “cửa sổ xoay vòng”: hệ thống phát key mới bằng secret mới, nhưng vẫn chấp nhận key cũ. Khi kết thúc, có thể xoá secret cũ.
Kết quả: thời gian xử lý giảm từ 2.5 giây xuống dưới 100ms.
Vài suy nghĩ cuối cùng về Django và kiến trúc của nó
Điều tôi yêu thích ở Django là khả năng mở rộng hành vi thông qua hệ thống class của Python. Bạn không cần viết decorator hoặc middleware phức tạp — chỉ cần kế thừa class, override, và gọi super() đúng lúc.
Ví dụ:
class IsActiveEntity(BasePermission): message = "Entity is not active." def has_permission(self, request, view): return request.user.is_active class HasActiveSubscription(IsActiveEntity): message = "Entity is not active or does not have an active subscription." def has_permission(self, request, view): if not super().has_permission(request, view): return False return getattr(request.user, "has_active_subscription", False)
Bạn có thể xây logic thành từng lớp nhỏ. Một class validate key, một class theo dõi usage, một cái rate limit — và tất cả hoạt động cùng nhau thông qua method override.
Tổng kết
Tôi vẫn dùng các công cụ hiện đại như Next.js, T3 Stack, Laravel, NestJS, Fastify... tùy dự án.
Nhưng Django vẫn phù hợp với tôi hơn. Nó cho tôi kiểm soát, cấu trúc, và hỗ trợ lâu dài (LTS) — điều mà hệ sinh thái JavaScript hiếm khi cung cấp.
Tất nhiên, nếu bạn không quen với Django, không biết chọn package nào, nó có thể trở nên nặng nề và chậm chạp. Django đòi hỏi bạn phải biết mình đang làm gì.
Và nếu Django không phù hợp với dự án hiện tại của bạn, cũng không sao. Hãy dùng cái gì phù hợp: FastAPI, Node.js, NestJS, Ruby, hay bất cứ framework nào đáp ứng đúng mục tiêu.
Quan trọng nhất là bạn hiểu mình đang tối ưu cho điều gì: tốc độ ra mắt, khả năng bảo trì, hay kỹ năng của team — và đưa ra quyết định phù hợp.
Đó là những gì giúp tôi phát triển nhanh.
Cảm ơn các bạn đã theo dõi!