▲ 12 r/django

DRF Auth Kit - The modern auth toolkit for Django Rest Framework

Hi guys, I want to (re)introduce DRF Auth Kit after a long time without talking about it, so I think it's worth bringing it up again and sharing some updates since my last post.

So, first of all, why would you ever need another auth package when we already have django-allauth, dj-rest-auth, djoser,... Here is the list of reasons why I created drf-auth-kit, which is used in production by me and many people, and actively maintained:

  • Full & strict type checking: mypy and pyright support (I plan to support ty after its beta) (something no other auth package has right now)
  • Strictly follows the OpenAPI schema (with drf-spectacular support) (only django-allauth had this at the time I created the package)
  • Dedicated to DRF, which means it's very easy to override any part: sign in, sign up (serializer, request, response)
  • Easy to use, based on the well-known django-allauth for social account and email management. I reuse those parts to avoid reinventing the wheel, while the other parts like serializers, views, and URLs have been designed based on my experience working with dj-rest-auth, django-trench, and djoser, for the best experience on the API.

Those are the key things I felt were lacking when I used other auth libs. And here are the features + updates since my last post:

  • Multiple authentication types: JWT (default), DRF token, or custom if you need (there's already an example)
  • Cookie-based security: HTTP-only cookies
  • Complete User Management: Registration, password reset, email verification, sign in.
  • (new) Multi-Factor Authentication: Supports multiple MFA methods with backup codes, including passkeys and hardware security keys
  • (new) Passwordless Authentication: Email magic links and passkey (WebAuthn) login
  • Social Authentication: Django Allauth integration with 50+ providers, supporting both OAuth2 and OpenID Connect.
  • Internationalization: Built-in support for 57 languages including English, Spanish, French, German, Chinese, Japanese, Korean, Vietnamese, and more
  • Full Type Safety: Complete type hints with mypy and pyright
  • OpenAPI Integration: Strictly best-practice auto-generated API documentation with DRF Spectacular
  • Flexible Configuration: Customizable serializers, views, and authentication backends
  • (Small extra): A UI (with the help of AI in this part) to easily try all the auth features quickly in dev/local environment

https://preview.redd.it/4ky62c1a35kh1.png?width=1442&format=png&auto=webp&s=0639c6c6f8b2727500dcf5395b4a0dc519284513

I have used it in production for a long time, and love it so much. I also actively maintain it and fix bugs raised by users. It's also listed in https://www.django-rest-framework.org/api-guide/authentication/#third-party-packages

Here is the info:

- Github: https://github.com/forthecraft/drf-auth-kit

- PyPI: https://pypi.org/project/drf-auth-kit/

Hope you guys love it as well. Feedback, feature requests, stars or improvements are welcome.

reddit.com
u/huygl99 — 1 day ago
▲ 24 r/FastAPI+1 crossposts

Composable, reusable WebSocket components for any ASGI framework (Django, FastAPI, Litestar)

Hi all, I'm the maintainer of a small channels (WebSocket) extension library for Django (and FastAPI too). While using and maintaining it, I started thinking it could become a small framework as well: composable and framework-independent, so it could be reused across Django/FastAPI/Litestar/... as long as the framework supports ASGI. Before going further, I'm putting the blueprint out here to compare notes with people who work with WebSockets regularly. If you have ever worked with WebSockets, I hope you can share any ideas, info, pain points, or suggestions you have.

Prerequisites, what my library already has:

  • Function-like handlers rather than while True + if/else
  • Automatic AsyncAPI doc generation
  • Full type hints
  • A testing kit
  • Support for all ASGI-based frameworks (Django, FastAPI, ...)

At a glance, it looks like this:

@ws_handler(output_type=ChatNotificationMessage)
async def handle_chat(self, message: ChatMessage) -> None:
    # Automatically routed, validated, and type-safe
    await self.broadcast_message(
        ChatNotificationMessage(payload=message.payload)
    )

@ws_handler
async def handle_ping(self, message: PingMessage) -> PongMessage:
    return PongMessage()  # Auto-documented in AsyncAPI

If you have ever worked with WebSockets, I think you get the idea of what it does here.

Recently I added the Topic feature, which is composable and reusable. It came out of a multiplexing feature request, and I was inspired by Phoenix Channels. It looks something like this:

class DiscussionTopic(Topic):
    pattern = "discussion:{pk}"

    async def authorize(self, pk: str) -> bool:
        return await user_can_view(self.scope["user"], pk)

    @ws_handler
    async def handle_reply(self, message: ReplyMessage) -> ReplyCreatedMessage:
        return ReplyCreatedMessage(payload=message.payload)

    @event_handler
    async def handle_new_reply(self, event: NewReplyEvent) -> ReplyCreatedMessage:
        return ReplyCreatedMessage(payload=event.payload)

And you use it like this:

class HubConsumer(AsyncJsonWebsocketConsumer):
    authenticator_class = JWTAuthenticator
    topics = [DiscussionTopic, RoomTopic]

In short, topics let you multiplex: subscribe, publish messages, unsubscribe, and so on, all over the same socket. So you can reuse a single WebSocket connection and just add or compose multiple topics, i.e. multiple WebSocket handlers.

That made me think: if we could create reusable topics such as Notification, Streaming, Voice, AI Agent, and so on, which users could easily install or copy and then modify or inherit from in a structured way, WebSocket handling would become much more structured and easier. The idea is similar to DRF and its ecosystem, and the composable/reusable part would work like shadcn: copy it, own it, and modify the code freely.

What would you use it for? As I mentioned above: notifications, streaming, voice, AI agents, and so on. I have done a lot of WebSocket work, and I keep having to redefine the same things over and over. There is no reusable approach like the ones we have for REST APIs. Another example is using Pydantic AI with the AG-UI protocol but over WebSockets, defined in a reusable way.

So, if you already know of an existing open source solution or library similar to this idea, it would be great if you could share it here. And if this resonates with you, a comment would help, both to add more insight and to give some encouragement to actually build this.

reddit.com
u/huygl99 — 4 days ago
▲ 10 r/django

Have you ever needed a composable, reusable WebSocket framework - like DRF, but for Channels?

Hi all, I maintain a utility package that extends Django Channels (WebSocket/ASGI). I'm thinking about building a small framework in the same spirit as DRF and its ecosystem, but aimed at Channels/WebSockets - things like audio streaming, notifications, bidirectional chat, chat rooms, and so on.

Before I start, I wanted to ask the community: have you ever needed something like this? For example, you had to implement a WebSocket feature, struggled with it, and went looking for a tutorial or library that already solved the problem and came up empty.

If these are real pain points, I think there's room for a reusable library. Looking forward to hearing your thoughts.

reddit.com
u/huygl99 — 7 days ago
▲ 57 r/Python

Benchmarking Python API frameworks with real workloads: FastAPI, Litestar, DRF, Ninja, Bolt

Hi guys, I benchmarked the well-known (and rising star) Python API frameworks - but with real production-shaped workloads, not just raw JSON echoes. Most comparisons out there are basically "hello world" benchmarks, while real APIs do auth, DB access and complex queries. So this measures those, with strict resource limits and each framework's own best practices.

Repo (code, full report, raw results): https://github.com/huynguyengl99/python-api-frameworks-benchmark

This is round 2 - last round's feedback (thanks especially to the Litestar author) directly shaped it: Litestar and Bolt now serialize with native msgspec instead of Pydantic (payloads byte-identical across frameworks), and everything is upgraded to latest (Django 6.0, FastAPI 0.141, Litestar 2.24, Bolt 0.10).

Setup

  • Each framework alone in a Docker container: 1 CPU, 750MB RAM, PostgreSQL 16
  • bombardier, 100 connections, 10s per endpoint
  • Median over 5 separate container starts (not best-of-N - some servers pick their throughput at startup, so best-of-N flatters the lucky ones)
  • 7 endpoints: 1KB/10KB JSON, simple DB reads, paginated articles with nested relations, article detail, and two JWT httpOnly cookie auth endpoints (each framework using its own ecosystem's auth library: AuthX, drf-auth-kit, django-ninja-jwt, or built-in support)

Key results (RPS)

(Images aren't allowed here - all graphs are in the repo README: https://github.com/huynguyengl99/python-api-frameworks-benchmark)

Config json-1k /db /articles /auth/me /auth/articles
bolt 38,576 1,986 208 3,024 196
litestar-uvicorn 31,284 1,039 246 976 193
litestar-granian 19,006 1,180 250 1,104 210
fastapi-uvicorn 13,845 984 224 820 193
drf-gunicorn 3,925 282 140 261 133
drf-granian 2,703 830 198 726 179
ninja-uvicorn 1,533 699 126 584 114
drf-uvicorn 1,035 495 153 447 137

(fastapi-granian and ninja-granian omitted for brevity - full table in the repo. Zero errors across all 70 measurements.)

Resource usage: most configs peak at 195-260MB RAM; drf-granian is the outlier at 456MB (untuned --blocking-threads, per the Granian maintainer). CPU: nearly everything saturates ~85% of the 1-CPU budget under load - except Bolt at 67%.

Takeaways

  • 37x spread on raw JSON collapses to ~1.9x once PostgreSQL is involved. For DB-heavy APIs (most of them), query optimization matters far more than framework choice.
  • Cookie JWT auth costs 5-20% on a DB-heavy endpoint. Bolt is near-free (it validates the JWT in Rust before Python runs); Litestar pays the most because its auth middleware opens a second DB session to load the user.
  • uvicorn vs granian isn't one-way: uvicorn wins CPU-bound JSON for ASGI frameworks, granian wins the DB-bound endpoints, and granian is clearly better for WSGI DRF.
  • Django Bolt is the one to watch: top spot on 4 of 7 endpoints at 67% average CPU while everyone else sits ~85%, and you keep the Django ORM/admin/ecosystem. Young, and its throughput varies between container starts under a hard CPU cap, but great for side projects already.
  • All caveats (including feedback I haven't addressed yet, like Granian's --blocking-threads) are documented in the repo's Methodology section.

If you find it useful, a star would encourage more deep dives like this - issues and PRs welcome, especially from people who know these servers better than I do.

reddit.com
u/huygl99 — 9 days ago
▲ 30 r/django

Python API Framework Benchmark round 2: now with JWT httpOnly cookie auth

Hi guys, I'm back on the Python framework benchmark after a busy few months. Many of you were interested last time (previous post), there was good feedback in the comments, and the frameworks have had updates since.

Repo (code, full report, raw results): https://github.com/huynguyengl99/python-api-frameworks-benchmark

What changed since last time

  1. Everything upgraded - Django Bolt 0.4.7 → 0.10.0, Django 6.0, FastAPI 0.141, Litestar 2.24, DRF 3.18, Django Ninja 1.6.2.
  2. Each framework now uses its own native serializer. The Litestar author pointed out in r/Python that feeding Litestar Pydantic models makes it convert twice, so I was benchmarking Pydantic instead of Litestar. Now Litestar and Bolt use msgspec structs, FastAPI and Ninja keep Pydantic, DRF keeps its serializers. Output is byte-identical between the two paths (4,796 bytes for /articles/1 from both FastAPI and Litestar).
  3. Two new endpoints using JWT in an httpOnly cookie - the auth setup I'd actually use in production. Each framework uses its own ecosystem's library instead of something hand-rolled: drf-auth-kit (DRF), django-ninja-jwt (Ninja), AuthX (FastAPI), and the built-in cookie JWT support in Litestar and Bolt.
  4. Reworked the measurement itself - fixed latency reporting, and each framework is now sampled over 5 separate container starts instead of repeated runs in one, so the numbers here are not directly comparable with my previous post.

TL;DR

  • Framework choice barely matters once you touch I/O. Same conclusion as last time, and it survived every fix. Optimize your queries, not your framework.
  • For raw JSON, Bolt and Litestar are clearly ahead, then FastAPI, then a big gap down to Ninja and DRF.
  • On DB endpoints everything compresses to about 1.9x between fastest and slowest.
  • Bolt is dramatically faster on the lightweight authenticated endpoint (/auth/me: 3,024 vs 1,104 for the next best) because it validates the JWT in Rust before Python runs at all. On the DB-heavy authenticated endpoint that advantage disappears.
  • "Granian beats uvicorn" is too simple. For ASGI frameworks, uvicorn wins the CPU-bound JSON endpoints (Litestar 31,284 vs 19,006) while Granian wins the DB-bound ones. For DRF, which is WSGI, Granian is much better than uvicorn - but see the memory note.
  • Django Bolt is the rising star here and I'd genuinely recommend trying it. It tops /json-1k (38,576), tops /db (1,986), is 2.7x ahead on /auth/me, and does it at 67% average CPU while everything else sits at ~85% - that headroom is the number I find most impressive. And you keep the Django ORM, admin and packages.

Setup

  • MacBook M2 Pro, 32GB RAM; PostgreSQL 16; 500 articles, 2000 comments, 100 tags, 50 authors
  • Each framework in its own container, --memory=750m --cpus=1, one at a time
  • 100 connections, 10s per measurement, bombardier
  • 5 independent container starts per framework, median reported with min-max spread

Endpoints: /json-1k, /json-10k, /db (10 rows), /articles?page=1&page_size=20 (paginated, nested author + tags), /articles/1 (nested author + tags + comments), /auth/me (cookie JWT → current user), /auth/articles (cookie JWT → the paginated query).

One detail that matters: all five frameworks load the user row from the DB on both auth endpoints. Litestar, DRF and Ninja do it as part of authenticating, while AuthX and Bolt's guards only verify the signature - so I made FastAPI and Bolt load the user explicitly. Otherwise they'd be doing strictly less work and the comparison would be junk.

Results

https://preview.redd.it/gdy1eq1iqdih1.png?width=2082&format=png&auto=webp&s=f4f6e9189466621ab51e65defdd89938f7042f8a

Config json-1k json-10k /db /articles /articles/1 /auth/me /auth/articles
bolt 38,576 19,089 1,986 208 432 3,024 196
litestar-uvicorn 31,284 24,547 1,039 246 443 976 193
litestar-granian 19,006 15,166 1,180 250 488 1,104 210
fastapi-uvicorn 13,845 2,641 984 224 428 820 193
fastapi-granian 8,484 2,280 952 201 410 749 199
drf-gunicorn 3,925 3,132 282 140 193 261 133
drf-granian 2,703 2,200 830 198 321 726 179
ninja-granian 1,566 1,422 680 130 295 610 117
ninja-uvicorn 1,533 1,424 699 126 236 584 114
drf-uvicorn 1,035 973 495 153 234 447 137

Zero errors across all 70 measurements.

The gap collapses, again

37x between fastest and slowest on /json-1k. 1.9x on /articles/1. Same story as last time, and it held up after all the methodology changes. If your endpoint touches PostgreSQL, your framework is not the bottleneck.

https://preview.redd.it/19jk8kcjqdih1.png?width=1483&format=png&auto=webp&s=8c21cf207d6c2035ba152ded04c09fafa80a0ff4

https://preview.redd.it/aldfey0kqdih1.png?width=1483&format=png&auto=webp&s=f95790b26901e9c9a74793c64e9ae525dffb6525

What authentication actually costs

https://preview.redd.it/jksmcw1lqdih1.png?width=1483&format=png&auto=webp&s=722e000986bc038516d8985762111d042f27b4d6

https://preview.redd.it/xjshe4slqdih1.png?width=1483&format=png&auto=webp&s=213a02d4a526997645d7a4a69bca21f8a8db54d3

Comparing /auth/articles against the identical unauthenticated /articles:

Config public authed cost
fastapi-granian 201 199 −1%
drf-gunicorn 140 133 −5%
bolt 208 196 −6%
drf-granian 198 179 −10%
ninja-uvicorn 126 114 −10%
fastapi-uvicorn 224 193 −14%
litestar-granian 250 210 −16%
litestar-uvicorn 246 193 −22%

So roughly 5-20%, cheaper than I expected for "verify a token and load a user". Litestar's is the highest, and there's a concrete reason: its JWTCookieAuth runs in middleware, before dependency injection, so retrieve_user_handler opens its own DB session - that request pays for two connection acquisitions instead of one.

Memory and CPU

https://preview.redd.it/z3a1h4omqdih1.png?width=2083&format=png&auto=webp&s=2a861a0b381740b5ce0491229f4a825e4dabcc54

Most configs peak at 195-260MB. drf-granian is the outlier at 456MB, and the Granian maintainer already explained why in the last thread (see below).

Bolt's number worth repeating: 67% average CPU while leading most endpoints, against ~85% for everything else.

A word on Django Bolt

If you're open to a young framework, this is the one I'd watch. It won or tied the top spot on 4 of 7 endpoints, and it did so while leaving ~18% more CPU headroom than every other config - that means room to grow under load, not just a good number on a chart. The /auth/me result (3,024 vs 1,104 for the runner-up) shows what you get when JWT validation happens in Rust before Python is even involved. And unlike moving to Litestar or FastAPI, you keep the Django ORM, admin and the package ecosystem.

Honest trade-offs: it's young and still moving fast, the Rust internals mean you can't monkey-patch your way out of a corner or contribute as easily, and under a hard 1-CPU cap its throughput varies noticeably between container starts (its Rust worker pool is sized from host cores, not the cgroup limit). But for a side project or a new internal service, I'd have no problem reaching for it today.

Feedback I have NOT addressed yet

Being upfront, because it affects one result:

u/gi0baro (Granian maintainer) explained that drf-granian's big memory number comes from me not setting --blocking-threads or backpressure, so it spawns a lot of threads and spends time on GIL contention. I still haven't set it - drf-granian's 456MB is that same unfixed issue. He also noted Granian runs I/O in a separate runtime with extra threads, so a 1-CPU cap penalizes it more than other servers, and that --cpus=1 in Docker is a time-slice scheduler limit, not a real core pin.

I'm keeping the CPU cap because it makes runs reproducible and comparable, but he's right that it isn't "one core" and right that Granian is disadvantaged by it. Tuning --blocking-threads is top of my list for round 3.

Also still not measured: cold start time and disk/image size, both suggested by the Litestar author last time.

Thanks to everyone who commented last time - the msgspec point and the Granian threading explanation both directly shaped this round. Issues and PRs very welcome, especially if you know these servers better than I do, and a star would be appreciated 😄

https://github.com/huynguyengl99/python-api-frameworks-benchmark

reddit.com
u/huygl99 — 10 days ago
▲ 14 r/FastAPIShare+1 crossposts

Open sourcing my typed WebSocket approach for building AI agents with Pydantic AI on FastAPI

Hey everyone, after taking a bit of a break I decided to write an in-depth tech blog to help those of you building (or about to build) an AI agent in the Python ecosystem, especially on FastAPI, so you can end up with something structured, scalable and efficient. This comes out of a lot of time working on chatbots, streaming and AI agents. There was no good guide for me when I built those, so after suffering plenty of pain and bugs I decided to write it up and open source part of my work to help you avoid the same. Hope it helps.

Full blog here in case you have time to read properly. Whether you're a team lead or senior who needs to build a scalable AI agent project, or a junior or intern who wants to build one the right way, this should be useful (it cost me a lot of time and money to learn): https://huynguyengl99.github.io/posts/pydantic-ai-typed-websockets-fastapi/

Comes with the repo: https://github.com/huynguyengl99/pydantic-ai-ws-agent

If you want to catch up first, TL;DR:

  1. A contract-based, structured protocol still beats an unstructured, code-first one. Instead of writing a pile of if/else and tests to make sure things are correct (and sometimes they still aren't), you keep one up-to-date contract that both the backend and the frontend build against, so no field is silently missing or outdated. For REST APIs that's OpenAPI. For WebSockets it's AsyncAPI. You can even design the whole protocol before writing any code. Trust me, it saves you a lot of 2am bugs.
  2. Prefer a structured, model-independent agent. There are plenty of options now (LangChain, the OpenAI SDK, the Gemini and Anthropic SDKs), but most of the time what matters is being able to switch model or provider without rewriting anything, structured and validated tool calls (without tool calls it's just a chat, not an agent), and something that fits your existing ecosystem. Tie yourself to one vendor's SDK and you'll feel it the first time you want to compare models or a provider has an outage. Pydantic AI is the one that gives you all of that. If you've used LangChain you'll remember the mess around messages and tool handling, a lot of unnecessary complexity. Pydantic AI keeps it structured, and since it comes from the Pydantic team it speaks Pydantic models natively, which is exactly why it fits FastAPI so well.
  3. Once your agent is structured, your server API should be too. Most people reach for SSE because it's simple, but it's missing a few things: bidirectional messages, a proper API schema (OpenAPI 3.2.0 has some support for streaming now, but tooling is still limited and it doesn't feel as natural as it does for REST), and any official way to offload work to a worker or push a message from outside the request, like a notification from somewhere else in your system. WebSockets don't have that schema problem, because AsyncAPI is a real spec built for exactly this: every message your server can send or receive, described in one document your frontend can generate a client from. It's the same deal OpenAPI gives you for REST, just for event-driven APIs.
  4. Bidirectional matters more than people expect, and human-in-the-loop is why. With Pydantic AI you can mark a destructive tool requires_approval=True. The run then stops before executing, hands you the pending calls, and you send an approval request to the UI. The user approves or denies (they can even edit the arguments), and you resume the run from where it paused. That round trip is awkward over SSE and completely natural over a WebSocket, it's just two more messages on a connection you already have.
  5. That's where ChanX and fast-channels come in. Based on the well-known django-channels package, I built fast-channels to bring the channels architecture to FastAPI. ChanX is the only tool I'm aware of that makes WebSocket handlers structured and easy while auto-generating AsyncAPI docs, so your frontend can generate a client straight from the schema. That's the contract you need to stop things going stale or breaking silently. It also gives you the offload story: the run happens in a background task and broadcasts to a conversation group, so a page refresh mid-run doesn't kill the answer, every tab stays in sync, and moving the work to a Celery or taskiq worker later changes nothing else.
  6. One more thing that surprised me: the whole flow becomes testable without an LLM. Because the protocol is typed and the agent is built from a factory, Pydantic AI's FunctionModel can script exactly what the model does. Streaming, tool execution, approve, deny and reconnect all become deterministic tests. Mine run in about a second with no API key, which is the difference between having tests and pretending to.

That's the core and the summary for a quick catch up. When you have time, reading the blog and looking at the repo will give you more insight into building a structured, scalable, production-ready AI agent.

Related repos:

If ChanX or fast-channels turn out to be useful for you, a star or any contribution to improve them would be appreciated 😄

I'm open to any discussion in the comments, feel free to bring up any problem you're hitting when building an AI agent. I'll try my best to help, since I've been through most of the pain already.

u/huygl99 — 13 days ago
▲ 15 r/django

In-depth DRF API design: choosing between APIView, ViewSet and the generic views

Hi all, taking a bit of a break so I thought I'd share the in-depth DRF API design approach I use. Hope it helps some of you design a better API system.

Something I notice in almost every DRF codebase, mine included for a long time: views land at one of two extremes. Either everything is an APIView with hand-written post() methods, or everything is a ModelViewSet copied from a tutorial. Generic viewsets, mixins and things like CreateAPIView never get used, mostly because it isn't obvious what problem they solve.

Here's the rule I ended up with, in the order I apply it.

1. If the endpoint touches the database, it's a viewset.

Anything model-backed is a resource with a lifecycle, even if you only expose two actions today. "I only need list and retrieve" isn't a reason to drop to APIView, it's a reason to compose:

class InvoiceViewSet(
    mixins.ListModelMixin,
    mixins.RetrieveModelMixin,
    GenericViewSet,
):
    queryset = Invoice.objects.all()
    serializer_class = InvoiceSerializer

You keep filtering, pagination, permission classes and correct schema generation for free, and the URL stays a resource instead of a pile of verbs.

2. APIView is only for things that aren't resource access at all.

Health checks, third-party callbacks. Webhooks do write to your DB, but as a side effect of an external event, not because someone is accessing a resource. Even there I declare a serializer, because a Stripe webhook is one of the highest-stakes endpoints you own and you want it validated and documented.

3. The concrete generic views are for /me style endpoints.

RetrieveUpdateDestroyAPIView and friends finally clicked for me here: /me, /workspaces/20/me. Real objects with a read/update/delete lifecycle, but the lookup comes from the session instead of an id in the URL:

class WorkspaceMeView(RetrieveUpdateDestroyAPIView):
    serializer_class = WorkspaceMemberSerializer

    def get_object(self):
        return get_object_or_404(
            WorkspaceMember,
            workspace_id=self.kwargs["workspace_id"],
            user=self.request.user,
        )

One class, one get_object, three methods. With APIView that's three views re-deriving the same object.

4. The serializer is what makes any of this pay off.

I disliked serializers at first, they felt like ceremony over a dict. Pairing them with drf-spectacular is what flipped it: get_serializer_class per action isn't just validation, it's what makes the generated docs precise enough that you can generate a typed frontend client straight from the schema.

Longer write-up with more code: https://huynguyengl99.github.io/posts/drf-view-classes-apiview-viewset-generic/

Hope it helps you level up your API design a bit. And if you have useful tips of your own, share them with the community.

u/huygl99 — 19 days ago
▲ 9 r/django

Advanced API development with DRF + React: schema-first, end-to-end types, auto-generated client and forms

Hi everyone, I want to share the setup my team has been using for a few years to eliminate a whole class of bugs: the backend renames a field, the frontend keeps sending the old one, and nothing complains until production. Since adopting this, we simply haven't faced outdated params or wrong response shapes anymore.

The core idea: the OpenAPI schema is the protocol between BE and FE. The backend generates it, the frontend generates FROM it, and type checkers on both sides refuse to compile anything that violates it.

https://preview.redd.it/7n8xlm1akmch1.png?width=1600&format=png&auto=webp&s=d81a3fcb5f110c51ec66b07754b98456d9e7e173

Backend side

Everything flows from the serializers, so the habit is: always declare serializer_class and queryset, override get_serializer_class() per action:

class ProjectViewSet(ModelViewSet[Project]):
    queryset = Project.objects.all()
    serializer_class = ProjectSerializer
    permission_classes = [IsAuthenticated, IsProjectMember]  # reused, not repeated

    def get_serializer_class(self) -> type[BaseSerializer[Project]]:
        if self.action == "list":
            return ProjectListSerializer  # lighter payload for lists
        return super().get_serializer_class()

drf-spectacular serves the schema live at /api/schema/, always in sync with the code by construction (in stricter environments you can commit an exported schema file instead, either works). Settings that matter:

  • COMPONENT_SPLIT_REQUEST: True is mandatory, otherwise read-only fields (id, timestamps) leak into request schemas and break FE mutations
  • Name enum fields by concept (task_status, not status) or they collide in the component registry
  • Polymorphic types need a postprocessing hook to force discriminators as required, or the generated validation marks them optional
  • CAMELIZE_NAMES: True if your JS consumers prefer camelCase

Typing: mypy strict (with the django-stubs plugin, since Django's metaprogramming needs it) plus pyright for fast in-editor feedback. Type serializers as ModelSerializer[Project].

Frontend side

With the backend dev server running, one command regenerates everything from /api/schema/ (openapi-zod-client with --group-strategy tag-file, wrapped in zodios):

pnpm gen:all
# → src/schemas/backend/   zod schemas, one file per OpenAPI tag
# → src/types/backend/     TypeScript interfaces
# → src/services/backend/  typed zodios API clients

const project = await projectsApi.projectsRetrieve({ params: { id } });
// rename a field on the backend, regenerate, and this line
// turns red before you even run anything

The zod schemas pay twice: the same generated schemas power runtime API validation AND form validation, up to fully auto-generated forms. When a serializer gains a field, the form grows it on the next regenerate - no manually synced form definitions.

Why DRF over Django Ninja for this: ViewSets model an API kind, not a function. You get reusable permission classes instead of decorating every endpoint, and the tags/grouping flow directly into the generated client structure.

Why the type hints and the contract matter so much

  • Type errors surface before tests even run. Together with tests, you get real confidence in the codebase, and IDE suggestions become genuinely good, which speeds up coding a lot
  • Honest note: you will probably hate strict typing for the first few weeks (I did, on both mypy and TypeScript). Push through it. Once it clicks, contract changes become impossible to miss and you start thanking the type checker for catching production bugs early
  • For teams, the contract, the tests, and the types are must-haves rather than nice-to-haves. They kill the silent, unspoken change: every contract change shows up in the generated files, so git and reviewers always see it, and lint/type checks fail if someone forgot to regenerate. Nobody has to remember to tell the frontend team

Bonus: this setup also makes AI coding assistants noticeably more effective. There's exactly one place to change (the serializer), everything downstream regenerates, and a hallucinated endpoint or param fails to compile instead of failing at runtime.

Full write-up with the complete configs and gotchas: https://huynguyengl99.github.io/posts/schema-first-api-development-drf-react/

If people are interested, I can put together a small open source demo project showing the whole pipeline end to end when I have some free time, so let me know.

Happy to answer any questions.

reddit.com
u/huygl99 — 1 month ago
▲ 25 r/django

Detect N+1 problems with nplus1 to improve Django performance

Hi everyone, I want to introduce an enhanced version of nplusone called nplus1.

The original nplusone has been unmaintained for around 8 years. I used it for a while and although it was helpful in many cases, I ran into false positives that forced me to whitelist a lot of things, and I also wanted nicer trace messages (inspired by django-zeal). So I decided to maintain and improve it.

A few things that are new or fixed compared to the original:

• Python 3.11+, full type hints (mypy strict + pyright strict)
• Django 4.2 to 5.2 support, SQLAlchemy 2.0 support
• No more false positives on nullable foreign keys (they are valid optimizations and now skipped)
• Proper handling of multi-table inheritance and polymorphic models via PK-based cross-model matching
• Skips checks on 4xx/5xx responses by default (configurable)
• Stack trace with registration site included in every detection message, so you know exactly where the offending query was set up
• New batch reporting mode that collects all detections and reports at the end of a request
• A NPLUSONE_ENABLED = False switch for zero overhead in prod
• Celery support out of the box
• Debug mode that logs every signal during a request

I have been using it in my real project and it works really well, even with complex Django patterns like polymorphic models. It catches almost all N+1 issues as well as redundant prefetch_related and select_related calls.

One thing to note: please only use this in your dev or test environment. The package uses middleware and monkey patches the ORM, so it is not meant for production. For production monitoring, tools like Sentry or Datadog are better suited. There is a NPLUSONE_ENABLED flag that makes it a no-op in prod if you want a single config.

I tested it intensively on Django. For SQLAlchemy and Peewee I mostly ported the original logic and the test suite passes, but I have not battle-tested those ORMs in a real project yet, so feedback is welcome.

Repo: https://github.com/huynguyengl99/nplus1

Hope you find it useful.

Disclaimer: I used Claude Code to help with parts of this, but I read every line, tested it against my own project, and have plenty of open source experience, so please do not write it off as AI slop. And again, since it is dev-only, there is no production performance concern.

reddit.com
u/huygl99 — 3 months ago