Today, it's easy to believe Django is outdated. Trendy tools and stacks like Next.js, Supabase, Astro, and T3 Stack are everywhere. They look fast and modern and come with many out-of-the-box features. You can spin up a full-stack app in a few hours and get moving quickly, doing the most important thing as a builder: providing value.
However, when I need to build a real SaaS backend that requires control, structure, and long-term maintainability, I still opt for Django as my first choice.
It's not always trendy, but it's dependable. And paired with the right tools, it's incredibly powerful.
What the Modern Tools Get Right
Let's be honest, Next.js and similar tools are impressive. You get frontend and backend in one place, deployment to Vercel is just a click away, and libraries like Prisma or Auth.js make setup fast and easy.
If you're building:
- A landing page
- A prototype
- A low complexity SaaS MVP
...the JavaScript stack works great. I've used it myself, and it helped me ship quickly. But I don't like the chaos that emerges once your application grows. And that's what I think Django manages better.
What Django Still Does Better
If Django and other tools are featured in "The Tale of the Rabbit vs. the Turtle," I believe Django is the turtle. When your product starts needing:
- More complex models
- A real API surface
- Fine-grained permissions
- Long-term control over your backend
...the fast-moving frontend-first stack starts to feel limiting.
Django, on the other hand, forces you to be explicit. You define your models, serializers, views, and routes. It may seem like more work upfront, but that structure pays off. Your logic stays clean. Your code is easier to debug. And when someone joins the project later, they don’t have to guess where things live.
Plus, with AI tools like Copilot, GPT, and Cursor, you can generate most of the “boilerplate” code in seconds:
“Write a Django REST Framework ViewSet for this model and register it in the router.”
What used to be repetitive is now just a prompt away. Finally, the turtle is more like a ninja turtle at the end of the day.
However, before stating that, when building my first API SaaS with Django, I encountered a problem: distributing API keys to our users.
API Keys with Django Is Hell
And no, JWT can't be used for API Keys. This is my opinion. One issue I see often in SaaS APIs is how developers handle authentication. Many organizations default to JWT tokens for all applications, users, machines, internal services, and partners.
But JWTs were made for user authentication, not for public-facing API access. But said like that is a little bit abstract, but here are some reasons why I don't use JWT for API Keys:
1. JWTs are Meant to Be Ephemeral
JWTs are typically issued after a successful authentication (login), and they:
- Contain encoded claims (like user ID, scopes, etc.)
- Have an expiry time (often 15 minutes to 1 hour)
- They are meant to be refreshed often via a refresh token
And there’s a reason for that: you can't revoke a JWT once it’s issued unless you maintain a token blacklist or rotate secrets aggressively. Its self-contained nature makes validation fast, but it also means that once a token is issued, it remains valid until it expires, regardless of the circumstances.
That's why short lifetimes and regular rotation are essential. But that’s also precisely what makes JWTs a poor fit for API key use cases, where keys might be issued once and used for weeks or months by machines or third parties. Adding a revocation system means registering the JWTs in the database, which is an anti-pattern to how JWTs are supposed to work.
2. JWTs Encourage Over-encoding
I've seen a codebase where developers often packed too much info into a JWT when using them as API keys:
{
"sub": "org_abc",
"role": "super",
"scopes": ["read", "write"],
"feature_flags": ["beta_search"],
"tenant_id": "xyz"
}
When the API Key is no longer used for permissions and authorization, it becomes a moving target with encoded business logic. Any mistake in token generation can compromise authorization and pose a security risk.
API keys should be:
- Opaque
- Simple
- Tied to metadata on the application, not logic in the client. The application handles all the metadata, what the key can do, who owns it, and whether it's active. The key itself has no logic or meaning embedded in it.
3. No Rotation Strategies
Well, imagine you've upgraded your authentication and API authorization system. With JWTs, you change the signing secret or key pair, and that's it. Great right?
Now, your clients are unhappy because their integration has stopped working suddenly. Every JWT you've issued prior has become invalid. With JWTs, there's no built-in way to support multiple active keys. You either:
- Accept downtime and reissue tokens, or
- Build a custom solution to manage various keys and key IDs (usually with extra middleware or a key registry)
That adds extra complexity to rotating credentials. Good API key systems should:
- Use key IDs to support multiple active secrets
- Allow graceful key rotation without breaking every client
- Can issue keys with fine-grained scopes or rate limits
You can't do this easily with standard JSON Web Tokens (JWTs).
It's essential to note that JWT can be easily customized to address these issues, as you will need to implement or correct these flaws manually. But for me, it's altering the standard. JWTs work great for users.
Coming back to Django and API keys, when I was building my first SaaS, a payment aggregator, I needed to implement API key authorization into the API. Thank God there was this package: Django REST Framework API Key. It was actually great, easy to implement, and got things working quickly.
However, I ran into a performance issue. The package generates a key representing the API key and then stores a hash of that key. The hashing is performed using Django’s password hashing framework, which is designed for security rather than speed. It’s resource-intensive and slow.
There was no problem during key creation, but the issue showed up during permission checks. When the API key is passed in the header, the package hashes it and then looks up the key in the database. The process becomes relatively slow if you stack several permissions with different logic.
My first optimization was to change the password hashing algorithm to Argon2. That helped—request times dropped from five seconds to around 2.5 seconds. Still, that wasn’t good enough for me.
Additionally, the package didn’t support rotation, so logging and tracking had to be implemented manually. It also didn’t fully follow Django’s standard authorization pattern.
In Django, permissions are usually checked against the request.user
, which is set during authentication. That gives you a lot of flexibility, even before permission logic is triggered.
And that’s when I decided to build something that I felt would be better and faster.
Building drf-simple-api-key
Fernet is a part of Python’s cryptography
package. It provides symmetric encryption with built-in support for multi-key rotation, which means you can encrypt something with one key and later decrypt it with another, without breaking anything. That’s exactly what you need when rotating API keys without downtime.
So I integrated it into a package I built: drf-simple-api-key.
I started small, trying to improve performance and match Django’s permission system more naturally. But over time, I added support for:
- API key authentication
- Key rotation
- Usage tracking and analytics
Here’s the core idea behind Fernet key rotation:
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)
If you rotate secrets properly, old keys continue to work, and new keys utilize the updated key. This avoids breaking every client at once, precisely the problem I faced.
To make rotation operational, I added a setting in drf-simple-api-key
that signals Django to start a rotation window. When this setting is enabled:
- The system generates new keys using the new secret
- But it still accepts and decrypts requests signed with old keys
- Once the rotation window ends, you can safely remove the old secret
This means rotation is smooth, and users don’t see any interruption: no mass invalidations, panicked clients, or backward-incompatible behavior.
It’s one of the most valuable tools I’ve built, not something I wrote entirely from scratch, as the Django REST Framework API key heavily inspires it.
The results? Went from 2.5 seconds to 100ms or under for requests. 🔥
However, let's take a moment to revisit Django's greatness.
Final Thoughts on Django and Its Architecture
One thing I appreciate about Django is how easily it allows for clean behavior extension through Python’s class system. You are not forced to write custom decorators or awkward middleware chains. You can rely on structured inheritance, define base classes, override methods, and call super()
exactly where it makes sense.
For example, in drf-simple-api-key
, I needed to integrate API key authentication with other features, such as usage tracking and rate limiting. Instead of rewriting the same checks in multiple places, I created a clean permission chain.
Here is a base permission that checks that an entity is active, which by default is the user.
class IsActiveEntity(BasePermission):
"""
A base permission that only checks if the entity (by default, the Django user) is active.
"""
message = "Entity is not active."
def has_permission(self, request: HttpRequest, view: typing.Any) -> bool:
return request.user.is_active
Now you want to create a permission that checks both that the entity is active and that they have a paid subscription.
You can extend the permission like this:
class HasActiveSubscription(IsActiveEntity):
"""
Extends IsActiveEntity and also checks for a valid subscription.
"""
message = "Entity is not active or does not have an active subscription."
def has_permission(self, request: HttpRequest, view: typing.Any) -> bool:
# First, run the base permission check
if not super().has_permission(request, view):
return False
# Then add your additional logic
user = request.user
return getattr(user, "has_active_subscription", False)
And that's one thing I like about Django: you can write exactly what needs to be modified, make the call you want, and intercept the state when and where you need it. That level of control is incredibly valuable, especially when your business logic doesn’t fit inside a preset workflow.
This allows me to build logic in layers, using inheritance and composition, without having to rewrite core behavior.
This is a typical Django and Django REST Framework (DRF) pattern. You see it in views, serializers, permissions, and middleware. It is not unique to my package, but it is one of the reasons Django scales well when your project grows.
When I built drf-simple-api-key
, this pattern helped me keep the logic modular. One class validated the key. Another tracked usage. Another applied limit. And all of it worked together through well-defined method overrides.
Final Thoughts
I still use recent and trending tools to build projects and sometimes SaaS for clients. I work with Next.js, the T3 stack, Laravel, Nest.js, Fastify, Hono, and much more.
But I think Django fits better. Maybe that’s just me.
It gives me more control. The stack is not fighting against me. And the support is solid. Django has real long-term support, LTS, a word I know the JavaScript ecosystem is unfamiliar with.
So, those other stacks are not automatically better. I think Django might be better than almost everything else out there. For me, it is one of the best software engineering tools ever made. That is not just an opinion; it is based on real experience.
That said, here’s the caveat.
If you don’t have the right tools around Django, don’t know the ecosystem, or are missing good packages, it can feel slow and heavy. Django does not hold your hand. It expects precision. Before you start, you need to know what you’re doing and what you are not doing.
And if Django doesn't fit your current project, that's fine. Use what works for you. Use FastAPI, Node.js, NestJS, Ruby, or any other framework that aligns with your goals. Too many excellent tools and libraries are available to become locked into a single mindset.
What matters is understanding what you're optimizing for: speed to market, maintainability, and the developer's skill set, and making your decisions accordingly.
This is what works fast for me.
If anything I said was helpful or you'd like to discuss authentication, API security, or Django packages, you can find me on Twitter. Feel free to reach out. And if you're trying to learn Django, I can point you to resources that helped me personally.
None of these are sponsored. Some are paid, some are free. I’ve bought them myself. They helped me understand things more clearly; I still reference them today.
- Django documentation: Still one of the most well-written documentation of a tool, in par with Laravel.
- Django course by Thinkster: Their course helped me understand DRF with concrete examples and also assisted in implementing JWT authentication. I'm unsure if the course has been updated, but the patterns are similar to what's done today.
- Test-Driven Development with Django, Django REST Framework, and Docker by Testdriven: An ideal course if you want to understand how to build an API with Django, Docker, add testing, and CI/CD pipelines.
- LearnDjango tutorials by Will Vincent: Vincent provides great tutorials and courses for learning Django. I followed the Django for APIs course, but didn't finish it because I was looking for a particular chapter. It's a well-written course, and you will find interesting resources on the website.
If you enjoyed this article and want more insights like this, subscribe to my newsletter for weekly tips, tutorials, and stories delivered straight to your inbox!
Top comments (2)
Kolawole, it's truly inspiring to see your enthusiasm for Django and its capabilities in crafting robust applications. While Django offers a comprehensive framework, I encourage you to explore FastAPI as well. FastAPI stands out for its exceptional performance, thanks to its asynchronous capabilities, which can significantly enhance the speed and efficiency of your applications. Its modern design allows for automatic generation of interactive API documentation, making it easier to understand and use.
FastAPI also supports data validation through Pydantic, ensuring that your data is clean and consistent. This feature can save you time and reduce errors in the development process. Additionally, FastAPI's flexibility allows for seamless integration with other libraries and tools, making it a versatile addition to your development toolkit.
You with FastAPI can open up opportunities for innovation and efficiency in your work.
This was a very nice read, and I agree with most of your takes. That LTS line caught me off-guard though. They're coming for you 😂
P.S: Good work with the API key package