<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Bytes of Django]]></title><description><![CDATA[Bytes of Django]]></description><link>https://soldatov-ss.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sun, 06 Sep 2026 14:18:59 GMT</lastBuildDate><atom:link href="https://soldatov-ss.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Almost Done: The Hidden Tradeoffs of Playwright E2E Testing in a Fast-Moving App]]></title><description><![CDATA[I kept thinking I was one fix away from done. Add a stable identifier here, patch a selector there, and the suite would finally go green. Then green would reveal the next thing underneath it. By the e]]></description><link>https://soldatov-ss.hashnode.dev/almost-done-the-hidden-tradeoffs-of-playwright-e2e-testing-in-a-fast-moving-app</link><guid isPermaLink="true">https://soldatov-ss.hashnode.dev/almost-done-the-hidden-tradeoffs-of-playwright-e2e-testing-in-a-fast-moving-app</guid><category><![CDATA[playwright]]></category><category><![CDATA[e2e testing]]></category><category><![CDATA[frontend testing]]></category><category><![CDATA[webdev]]></category><category><![CDATA[test-automation]]></category><category><![CDATA[page-object-model]]></category><category><![CDATA[Test Maintenance]]></category><dc:creator><![CDATA[Serhii Soldatov]]></dc:creator><pubDate>Sun, 07 Jun 2026 16:14:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68e28793524135bc3bf4e0b1/a3725140-c7d4-461a-9a4a-22827da48c6e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I kept thinking I was one fix away from done. Add a stable identifier here, patch a selector there, and the suite would finally go green. Then green would reveal the next thing underneath it. By the end of the week I'd learned something I hadn't known going in: this is just how end-to-end testing behaves on a product whose UI changes every release. My planning was never the issue.</p>
<p>The premise was almost too clean. The goal was modest: a test that clicks through creating a campaign the way a real user would, so we'd know the moment a merge broke it. You record that flow once with Playwright's codegen, paste the raw output into a Claude skill, and get back a structured test. No writing selectors by hand, no describing every button. Record, convert, done. I built that workflow, and it worked: the first test went green. That's the dangerous part — the surface really does deliver, which is exactly why you trust it before you've seen what's underneath.</p>
<h2>The first cost: the app has to be built to be recorded</h2>
<p>Codegen can only record what the page makes addressable. Point it at a button with visible text and you get a clean selector. Point it at a three-dot actions menu with no label, and you get <code>getByRole('button').nth(5)</code>, an index that breaks the instant a row reorders. Point it at one of our react-select dropdowns and codegen records <code>.emotion-class-nxiuxh-container &gt; … &gt; .emotion-class-11rjtvl</code>, a chain of generated CSS classes that won't survive the next restyle.</p>
<p>So before you record anything, every interactive element in the flow needs a stable identifier added to the source. Not all of them had one. That meant an audit-and-prep pass through the components first, and another pass every time a new flow gets added later. The "record" step everyone pictures as step one is actually step two.</p>
<h2>The second cost: conversion is its own negotiation</h2>
<p>Say you've prepped the app and recorded the flow. The conversion still isn't one-click. The skill turns most of the recording into a clean test, then stops at the parts it can't resolve on its own: a selector it doesn't recognize, an element that's ambiguous, a dropdown that still came through as an emotion class. For each one it asks a question, and answering it means going back into the source: add an <code>inputId</code> here, a <code>data-testid</code> there, re-record or hand-patch. A recording is just a list of clicks. The real work is reconstructing the intent behind them, and that work lives half in the test file and half back in the components.</p>
<p>And that's the cost for a short flow. The flows actually worth testing aren't short.</p>
<h2>The third cost: the most valuable tests are the most fragile</h2>
<p>The whole point of end-to-end testing is the long flow — create a campaign, then a line item inside it, then a visual on top of that. Fifty to a hundred interactive elements in a single chain, the exact journey a real user takes and the exact journey no unit test can cover. That's where the value is. It's also where the fragility is, and it's the same property producing both: the test is long because the flow is long, and every step in it is something that can change. A relabeled field in the middle, a reordered step, a drawer that now opens differently — any one of them breaks the chain from that point on, and a single UI tweak can turn into hours of tracing which step actually moved. The Page Object Model helps here: fix the selector in one class and every test that uses it recovers. But it only softens the blow, it doesn't stop the punches. The flows you most want to protect are the ones that break most often.</p>
<h2>The fourth cost: the test data you can't take back</h2>
<p>The first three costs are all about getting a test written and keeping it green. The fourth is different in kind. It's about what a test leaves behind. A real campaign flow doesn't just touch our own database; it creates entities on TikTok and Facebook. Internal records you can clean up: record the ID, fire a delete in <code>afterEach</code>, and a nightly cron sweeps whatever a crashed run left orphaned. External entities you can't, at least not reliably. Each platform has its own deletion rules, its own delays, its own idea of what's even allowed. There's no scheduled job that dependably undoes them. And unlike the other three, this cost doesn't shrink as the suite matures. It's a standing risk that grows with every test that touches a real platform.</p>
<h2>The cost doesn't end — that's the finding</h2>
<p>Here's what ties the four together. A normal feature has a build cost, and then it's done. These costs don't behave that way. The prep, the conversion fixups, the broken chains, the orphaned external data — each one recurs every time the UI moves, and on this product the UI moves every release. You don't build the suite once and walk away. You keep paying for it, and the bill scales with exactly the velocity that makes the product worth testing.</p>
<p>That's why I stopped thinking I was one fix away from done. There was no "finish" to reach at the cost I'd assumed; I just hadn't seen that yet. So the recommendation I brought back wasn't a test suite. It was: pause E2E investment for now, and keep the groundwork in place — the Page Object structure, the recording workflow, the selector conventions, the conversion skill all stay, ready if we ever decide the trade is worth it. The investigation produced something better than the thing we set out to build: a clear-eyed reason not to build it yet, and that turned out to be worth more than the suite would have been.</p>
<h2>If you're about to add Playwright to your project</h2>
<p>Here's what to actually expect, in plain terms:</p>
<table>
<thead>
<tr>
<th>What you're signing up for</th>
<th>When it costs you</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Prep before you can record.</strong> Every element in a flow needs a stable identifier in the source first.</td>
<td>One upfront audit per app, then a smaller pass <em>every time you add a new flow</em>.</td>
</tr>
<tr>
<td><strong>Cleanup after recording.</strong> The auto-conversion gets you most of the way; the rest is manual fixes back in the source.</td>
<td>A chunk of time <em>per flow</em>, every flow. It never drops to zero.</td>
</tr>
<tr>
<td><strong>Maintaining long flows.</strong> The high-value multi-step tests break whenever a step in the middle changes.</td>
<td>Recurring, <em>per UI change</em>. A single tweak can cost hours of tracing.</td>
</tr>
<tr>
<td><strong>External test data.</strong> Entities created on TikTok/Facebook can't be reliably auto-deleted.</td>
<td>Ongoing risk that never resolves; it grows with every test that touches a real platform.</td>
</tr>
</tbody></table>
<p>None of these are one-time. The setup is the part you can budget for; the rest you pay on every release.</p>
]]></content:encoded></item><item><title><![CDATA[How I added project-aware tab completion to Django's manage.py]]></title><description><![CDATA[Django technically ships with a bash completion script — it lives in extras/django_bash_completion. But nothing wires it up when you pip install django. You have to find it, source it manually, and on]]></description><link>https://soldatov-ss.hashnode.dev/how-i-added-project-aware-tab-completion-to-django-s-manage-py</link><guid isPermaLink="true">https://soldatov-ss.hashnode.dev/how-i-added-project-aware-tab-completion-to-django-s-manage-py</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[Open Source]]></category><category><![CDATA[webdev]]></category><dc:creator><![CDATA[Serhii Soldatov]]></dc:creator><pubDate>Thu, 21 May 2026 13:45:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68e28793524135bc3bf4e0b1/1deeb377-cade-4e47-9b25-5dd765991d2f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Django technically ships with a bash completion script — it lives in <code>extras/django_bash_completion</code>. But nothing wires it up when you <code>pip install django</code>. You have to find it, source it manually, and once you do, it completes command names and flags. That's it.</p>
<p>App labels? Migration names? No.</p>
<p>This has been an open issue since <a href="https://code.djangoproject.com/ticket/1240">ticket #1240</a>, filed in 2006. The discussion already identified app-name completion as the tricky part. Twenty years later it still isn't in core.</p>
<p>I built <a href="https://github.com/soldatov-ss/django-completion">django-completion</a> to fix it. This article covers that.</p>
<hr />
<h2>The daily friction</h2>
<p>The thing that bothered me wasn't forgetting command names. It was the two steps right after:</p>
<pre><code class="language-bash">python manage.py migrate acc&lt;Enter&gt;           # too early — typo
python manage.py migrate accounts 0003&lt;Tab&gt;  # what's the exact filename?
</code></pre>
<p>Guessing migration names is the worst. They follow a pattern (<code>0003_add_user_profile</code>) but you have to check the filesystem or run <code>showmigrations</code> first. Either way, you've left the flow.</p>
<hr />
<h2>Why argparse completion doesn't reach this</h2>
<p>Tools like <a href="https://github.com/kislyuk/argcomplete">argcomplete</a> or Django's own <code>django_bash_completion</code> parse the command's argparse definition and surface flags: <code>--verbosity</code>, <code>--settings</code>, <code>--no-input</code>, etc.</p>
<p>What they can't surface is positional arguments that come from <em>your project</em> — app labels, migration names, test module paths. Those aren't declared in argparse. They're discovered at runtime by loading <code>DJANGO_SETTINGS_MODULE</code> and calling <code>django.setup()</code>, which initializes the full app registry.</p>
<p>Doing that on every keypress is the wrong tradeoff. On a medium-sized project, <code>django.setup()</code> takes 200–500ms. That's noticeable as input lag, and the approach doesn't compose well with virtual environments or <code>uv</code>.</p>
<hr />
<h2>What django-completion does differently</h2>
<p>Instead of loading Django at completion time, the approach is inverted:</p>
<p><strong>Discovery happens after commands run, not when Tab is pressed.</strong></p>
<p>After any <code>manage.py</code> command finishes, django-completion rebuilds a small JSON cache at your project root. Tab completion reads that file. No Django import, no database access, no perceptible delay.</p>
<hr />
<h2>Install</h2>
<pre><code class="language-bash">pip install django-completion
</code></pre>
<p>Add to <code>INSTALLED_APPS</code>:</p>
<pre><code class="language-python">INSTALLED_APPS = [
    # ...
    "django_completion",
]
</code></pre>
<p>Run once to wire up the shell script:</p>
<pre><code class="language-bash">python manage.py autocomplete install
</code></pre>
<p>Restart your terminal. Tab works.</p>
<p>All common invocation forms are covered:</p>
<pre><code class="language-bash">manage.py migrate &lt;TAB&gt;
./manage.py migrate &lt;TAB&gt;
python manage.py migrate &lt;TAB&gt;
python3 manage.py migrate &lt;TAB&gt;
uv run python manage.py migrate &lt;TAB&gt;
</code></pre>
<hr />
<h2>How it works</h2>
<p>After any management command finishes, <code>AppConfig.ready()</code> has patched <code>BaseCommand.execute</code> to spawn a background thread that rebuilds the cache if it's stale. The patch looks like this:</p>
<pre><code class="language-python">def patched(cmd_self, *args, **kwargs):
    try:
        return original_execute(cmd_self, *args, **kwargs)
    finally:
        thread = threading.Thread(target=maybe_refresh_cache)
        thread.start()
</code></pre>
<p>The thread writes <code>.django-completion-cache.json</code> to <code>BASE_DIR</code> — command names, app labels, per-command flags, migration filenames. A 60-second cooldown prevents redundant rebuilds when you run several commands in a row. The cache refresh doesn't block your command at all.</p>
<p>When you press Tab, the shell reads that file and passes the current line to a small Python helper that applies a declarative rule table:</p>
<pre><code class="language-python">_COMMAND_RULES = {
    "migrate":        CommandRule(pos2="migration_apps",  pos3="migration_names"),
    "showmigrations": CommandRule(pos2="migration_apps",  pos3="options"),
    "makemigrations": CommandRule(pos2="local_apps",      pos3="options"),
    "sqlmigrate":     CommandRule(pos2="migration_apps",  pos3="migration_names_no_zero"),
    "dumpdata":       CommandRule(pos2="all_apps",        pos3="options"),
    "test":           CommandRule(pos2="all_apps",        pos3="options"),
}
</code></pre>
<p><code>migrate</code> at position 2 gets app labels filtered to those that have migrations. At position 3 it gets migration names for the app already typed, plus <code>zero</code>. A word starting with <code>-</code> always triggers flag completion. Commands not in the table fall back to flags only — a safe default for custom management commands.</p>
<p>Two smaller details worth naming: the shell script walks up from <code>$PWD</code> to find the cache file, so completion works from any subdirectory in your project. And <code>makemigrations</code> only surfaces <em>local</em> apps — ones inside <code>BASE_DIR</code> — so <code>django.contrib.admin</code> and friends don't clutter the list.</p>
<hr />
<h2>What's next</h2>
<ul>
<li><p><strong>Fish shell</strong> — the completion API is different from bash/zsh; the cache format is stable if anyone wants to write a Fish module</p>
</li>
<li><p><strong>PowerShell</strong> — out of scope for me personally, same offer</p>
</li>
<li><p><strong>Fuzzy matching</strong> — there's a stub in the codebase; not shipped yet</p>
</li>
</ul>
<hr />
<h2>Try it</h2>
<pre><code class="language-bash">pip install django-completion
</code></pre>
<ul>
<li><p>GitHub: <a href="https://github.com/soldatov-ss/django-completion">github.com/soldatov-ss/django-completion</a></p>
</li>
<li><p>Docs: <a href="https://soldatov-ss.github.io/django-completion">soldatov-ss.github.io/django-completion</a></p>
</li>
<li><p>PyPI: <a href="https://pypi.org/project/django-completion">pypi.org/project/django-completion</a></p>
</li>
</ul>
<p>If it breaks on your setup — unusual <code>BASE_DIR</code>, custom <code>MIGRATION_MODULES</code>, monorepo layout — open an issue. Those edge cases are the interesting ones.</p>
<p>If it works, a ⭐ on GitHub helps other Django developers find it.</p>
]]></content:encoded></item><item><title><![CDATA[Django Without the Mess: Repositories for Data, Services for Rules]]></title><description><![CDATA[The Elephant in the Room: Django's Fat Model Problem
Let’s talk about the elephant in every Django project: that one models.py file. You know the one. It started pure and simple, a beautiful reflectio]]></description><link>https://soldatov-ss.hashnode.dev/django-without-the-mess-repositories-for-data-services-for-rules</link><guid isPermaLink="true">https://soldatov-ss.hashnode.dev/django-without-the-mess-repositories-for-data-services-for-rules</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[clean code]]></category><category><![CDATA[Clean Architecture]]></category><category><![CDATA[design patterns]]></category><category><![CDATA[architecture]]></category><dc:creator><![CDATA[Serhii Soldatov]]></dc:creator><pubDate>Sun, 07 Sep 2025 11:58:40 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1759677448666/48ade256-d775-42b8-a83e-1a4f0f9c8fa0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>The Elephant in the Room: Django's Fat Model Problem</h2>
<p>Let’s talk about the elephant in every Django project: that one <code>models.py</code> file. You know the one. It started pure and simple, a beautiful reflection of your database schema. Now, it's a 2,000-line behemoth, bloated with custom managers, <code>@property</code> decorators that hide monstrous queries, and <code>save()</code> methods that seem to have more side effects than a late-night infomercial product.</p>
<p>Your views are a mess, your business logic is scattered, and making any change feels like performing surgery with a butter knife. In this moment of desperation, a seemingly elegant solution appears: "selectors." Just pull all those messy queries out into a separate <code>selectors.py</code> file! It feels clean. It feels right.</p>
<p>But I'm here to tell you that selectors are a trap. They’re a quick fix that feels like progress but ultimately leads you down a path of architectural pain. There’s a better way, a more structured approach that will save you from future headaches: the Repository and Service patterns. Let’s break down why this combination wins, and why selectors just don’t hold up in the long run.</p>
<hr />
<h2>How We All Got Here</h2>
<p>No one sets out to write messy code. We typically arrive here by following well-intentioned advice. In the Django world, the classic mantra is "fat models, thin views." The idea is to keep your views lean and push all the logic related to your data into the corresponding model.</p>
<p>And for a small project, this works beautifully! A <code>User</code> model with a method like <code>is_profile_complete()</code> is perfectly reasonable. But what happens when the logic gets more complex? Soon, you have <code>user.get_recent_orders()</code>, <code>user.calculate_lifetime_value()</code>, and <code>user.send_password_reset_email()</code>. Your model is no longer just a data structure; it's a tangled web of business rules, database queries, and external interactions.</p>
<p>This is when developers, rightly, look for a way to clean up. The <code>selectors.py</code> file is born. You create functions like <code>user_get_active_with_recent_orders()</code> and <code>product_get_top_sellers()</code>. The model slims down, the view calls a clean function—problem solved, right?</p>
<p>Not quite. Honestly, this is like sweeping a pile of dirt under the rug. The room looks cleaner at a glance, but you haven't actually dealt with the mess. You’ve just moved it. This separation is superficial, and as we’ll see, it creates a whole new set of problems.</p>
<hr />
<h2>Repository Pattern 101: Your Data's Bodyguard</h2>
<p>So, what’s the alternative? Let’s start with the first piece of the puzzle: the <strong>Repository Pattern</strong>.</p>
<p>In the simplest terms, a repository is an abstraction layer that sits between your application's business logic and your database. Its one and only job is to handle data access. Think of it as a specialized agent for a particular model. If you need to get users, save a user, or delete a user, you talk to the <code>UserRepository</code>. You design it to serve your use cases, not to mirror every possible query.</p>
<p><strong>Structure</strong>:</p>
<pre><code class="language-yaml">your_app/
  users/
    models.py
    repositories/
      __init__.py
      users.py
    services/
      __init__.py
      users.py
</code></pre>
<p>And here’s what that <code>UserRepository</code> might look like, inheriting from a <code>BaseRepository</code> (like the one <a href="https://gist.github.com/soldatov-ss/ad8a158b5e65308c952553bbe451fda9">here</a>):</p>
<pre><code class="language-python"># your_app/users/repositories/users.py

from your_app.apps.common.repositories import BaseRepository 
from your_app.users.models import User
from typing import Optional


class UserRepository(BaseRepository):
    def get_by_id(self, user_id: int) -&gt; Optional[User]:
        return self.filter(id=user_id).first()

    def get_by_email(self, email: str) -&gt; Optional[User]:
        return self.filter(email=email).first()
</code></pre>
<p><strong>Why this matters</strong></p>
<ul>
<li><p><strong>Clear contract.</strong> It provides a single, explicit interface for data operations. No more <code>User.objects.filter(...)</code> scattered across 20 different files. You centralize your query logic in one place.</p>
</li>
<li><p><strong>Centralized query intent.</strong> If a flow needs special prefetch or annotations, you add a method and keep it consistent.</p>
</li>
<li><p><strong>Test seams.</strong> You can pass a fake repository in tests. You can add caching or logging in one place without hunting through views.</p>
</li>
</ul>
<hr />
<h2>The service layer, also known as the brain</h2>
<p>If repositories are your data's bodyguards, services are the brain of the operation. Services sit above repositories to orchestrate business workflows and enforce rules. They are where your transactions should begin and end. If a business rule changes—like "premium users get priority processing"—you want one, and only one, place to make that change.</p>
<p>The best analogy is this: <strong>Repositories fetch the ingredients. Services follow the recipe to cook the meal.</strong></p>
<p>A service doesn't know <em>how</em> the ingredients are fetched (that's the repository's job), and the view doesn't know <em>how</em> the meal is cooked. The view just says, "I'd like to order the 'New User Registration' please."</p>
<pre><code class="language-python"># users/services/users.py

from .repositories import UserRepository
# Assume a NotificationService exists elsewhere
from notifications.services import NotificationService

class UserService:
    def __init__(self, user_repo: UserRepository, notification_service: NotificationService):
        self.user_repo = user_repo
        self.notification_service = notification_service

    def register_user(self, email: str, name: str) -&gt; User:
        # Business rule: check if user already exists
        if self.user_repo.get_by_email(email):
            raise ValueError("User with this email already exists.")

        # Step 1: Create the user via the repository
        user = self.user_repo.create(email=email, name=name)

        # Step 2: Orchestrate another action
        self.notification_service.send_welcome_email(user.email)

        return user
</code></pre>
<p>This is clean, transactional, and easy to follow.</p>
<hr />
<h2>The Real Risk of Selectors: A Slippery Slope</h2>
<p>So if services are the brain, why not just use selectors for the data? The risk isn't that selectors are inherently bad, but that they are a <strong>slippery slope</strong>.</p>
<p>They lack a strong architectural boundary, making it too easy to add "just one little piece" of business logic. Over time, these small compromises turn your clean query file into a tangled, secondary business logic layer.</p>
<p>The Repository and Service pattern prevents this by creating <strong>explicit guardrails</strong>.</p>
<ul>
<li><p><strong>Repositories</strong> have one job: get data.</p>
</li>
<li><p><strong>Services</strong> have one job: enforce business rules.</p>
</li>
</ul>
<p>This clear separation makes the right path the easy path and prevents the slow decay of your architecture.</p>
<hr />
<h2>Admin actions, celery tasks, and the rest of the crew</h2>
<p>Keep the same pattern everywhere. Admin actions call services. Celery tasks call services. Management commands call services. DRF viewsets and APIViews call services. One flow. One place for idempotency and retries. If a task runs twice, the service checks state and either skips or continues.</p>
<hr />
<h2>Wiring it together: <strong>init</strong>.py as a composition root</h2>
<p>Once you have repositories and services, you still need to instantiate them somewhere. A clean way to handle this is to use the package's <code>__init__.py</code> as a <strong>composition root</strong>: one file that wires dependencies together and exports ready-to-use singletons.</p>
<pre><code class="language-plaintext">users/
  services/
    __init__.py   ← wires everything here
    users.py
    notifications.py
</code></pre>
<pre><code class="language-python"># users/services/__init__.py

from .notifications import NotificationService
from .users import UserService
from users.repositories import user_repository

notification_service = NotificationService()
user_service = UserService(
    user_repo=user_repository,
    notification_service=notification_service,
)

__all__ = [
    "user_service",
    "notification_service",
]
</code></pre>
<p>Now callers import a single, already-wired object:</p>
<pre><code class="language-python"># instead of this
from users.services.users import UserService
user_service = UserService(user_repo=..., notification_service=...)

# just this
from users.services import user_service
</code></pre>
<p>This keeps instantiation in one place, hides the internal module structure, and makes the public API explicit via <code>__all__</code>.</p>
<p><strong>One tradeoff to know:</strong> services are instantiated at import time. If a test needs different wiring — say, a fake repository — it can't swap the dependency before the module loads. The fix is <code>unittest.mock.patch</code> on the singleton, or instantiating locally in the test. Neither is painful, but it's worth knowing before you adopt the pattern.</p>
<hr />
<h2>Common questions you will hear on the team</h2>
<ul>
<li><p><strong>Can I keep a small helper on the model.</strong> Yes, if it only touches that model’s state.</p>
</li>
<li><p><strong>Do I always need a service.</strong> If a view is a straight read, repo to serializer can be fine. When you branch or change state, reach for a service.</p>
</li>
<li><p><strong>Do I need a repo per model.</strong> Start where it helps most. Big aggregates, heavy queries, hot paths. Let the layer grow with need.</p>
</li>
<li><p><strong>Can selectors live beside repos.</strong> If they exist, make them thin wrappers over repos, not a separate source of truth.</p>
</li>
<li><p><strong>How does this fit with DRF?</strong> Your viewset or <code>APIView</code> calls the service, then passes the result to a serializer. The service never touches the request object or serializer context. That boundary keeps your business logic framework-agnostic — the same service works from a Celery task or a management command without modification.</p>
</li>
</ul>
<hr />
<h2>Why this matters on teams, not only in code</h2>
<p>Clean layers make it easier to talk. A PM says, payment failed on retry. You look at one place. A junior asks, where should I put this logic. You have a ready answer. A reviewer reads a PR and knows exactly which patterns to expect. The shape of the system stays steady as the people change.</p>
<p>Also, repositories act like discreet places to teach performance. You can add little comments, like why you used <code>select_for_update</code> here, or why an index matters. New teammates learn by seeing the same pattern in many methods. That is softer than sending a long wiki doc that nobody reads twice.</p>
<hr />
<h2>Conclusion: Choose Clarity Over Convenience</h2>
<p>Look, selectors aren't born from malice. They come from a good place: the desire to clean up messy code. They offer a moment of relief, a feeling of organization. But it’s a temporary fix. They are a crutch, not a long-term architectural strategy.</p>
<p>The Repository and Service patterns require a bit more discipline upfront. You have to think about your application's layers and enforce those boundaries Keep models slim. Put data access in repositories. Put rules in services. Own transactions at the service layer. Be consistent.</p>
]]></content:encoded></item><item><title><![CDATA[Part 2: Django REST Framework: When (and When Not) to Override Serializers and Viewsets]]></title><description><![CDATA[DRF, Part 2: ViewSet Overrides
Welcome to the second half of our guide on DRF architecture. In Part 1, we established a clear rule: serializers are the guardians of your data. They handle validation, ]]></description><link>https://soldatov-ss.hashnode.dev/part-2-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets</link><guid isPermaLink="true">https://soldatov-ss.hashnode.dev/part-2-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets</guid><category><![CDATA[Django]]></category><category><![CDATA[django rest framework]]></category><category><![CDATA[Python]]></category><category><![CDATA[clean code]]></category><dc:creator><![CDATA[Serhii Soldatov]]></dc:creator><pubDate>Sun, 10 Aug 2025 14:55:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68e28793524135bc3bf4e0b1/bf66a44c-eb9d-43a7-99fa-bfbfce88be43.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>DRF, Part 2: ViewSet Overrides</h2>
<p>Welcome to the second half of our guide on DRF architecture. In <a href="https://dev.to/soldatov-ss/part-1-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets-11a7">Part 1</a>, we established a clear rule: serializers are the guardians of your data. They handle validation, transformation, and representation. They ensure that the data entering your system is clean and the data leaving is well-formed.</p>
<p>But that’s only half the picture.</p>
<h2>The ViewSet's Core Mission: Orchestration</h2>
<p>Viewsets handle the HTTP journey. They don't care if a <code>start_date</code> is before an <code>end_date</code> — that's the serializer's job. They care about things like:</p>
<ul>
<li>Parsing the incoming <code>request</code>.</li>
<li>Checking permissions and authentication.</li>
<li>Calling the serializer to validate data.</li>
<li>Triggering the save operation.</li>
<li>Formatting the final <code>Response</code> with the right data and status code.</li>
</ul>
<p>Let's explore the most common and powerful methods you can override.</p>
<h2>The Big Showdown: <code>create</code> vs. <code>perform_create</code></h2>
<p>This is where so much confusion happens. Both seem to do the same thing! But their purpose is completely different, and choosing the right one is key to clean code.</p>
<ul>
<li><p>Override <code>create(self, request, *args, **kwargs)</code> when you need to change the <strong>orchestration of the request or the shape of the final response</strong>. Do you need to return a <code>202 Accepted</code> instead of a <code>201 Created</code>? Do you need to wrap the response in a <code>{ "data": ... }</code> envelope? Do you need to do something before the serializer is even validated? That's a job for create.</p>
</li>
<li><p>Override <code>perform_create(self, serializer)</code> when you just need to <strong>tweak how the object is saved</strong>. This is a much cleaner, more focused hook. The default create method calls this after validation is successful. It's the perfect place to inject the current user or tenant ID right before saving.</p>
</li>
</ul>
<p><strong>A practical rule of thumb</strong>: Always try to use <code>perform_create</code> first. Only override the full <code>create</code> method if you absolutely have to manipulate the HTTP response or the flow itself.</p>
<pre><code class="language-python"># In your OrderViewSet
class OrderViewSet(viewsets.ModelViewSet):
    # ... queryset and serializer_class definitions ...

    # This is the CLEAN way to stamp the user
    def perform_create(self, serializer):
        # The serializer is already validated here.
        serializer.save(customer=self.request.user)

    # Only override this if you need to change the whole dance
    def create(self, request, *args, **kwargs):
        # Maybe you need to check inventory before even trying to validate
        if not check_inventory(request.data):
            return Response({"error": "Item out of stock"}, status=status.HTTP_400_BAD_REQUEST)

        # Now call the original 'create' logic
        return super().create(request, *args, **kwargs)
</code></pre>
<p>This principle of separating the HTTP-level orchestration (create) from the persistence hook (perform_create) is key. Now, let's apply that same logic to updates.</p>
<hr />
<h2>The Update Trinity: <code>update</code> (PUT), <code>partial_update</code> (PATCH), and <code>perform_update</code></h2>
<p>Updates are more complex than creates because there are two different ways to do them: a full update (<code>PUT</code>) and a partial one (<code>PATCH</code>). DRF gives you three distinct methods to control this process.</p>
<p>First, let's get the flow right. It's a common point of confusion. The methods <code>update</code> and <code>partial_update</code> are peers; one does not call the other.</p>
<ul>
<li><p>A <code>PUT</code> request is routed to the <code>update()</code> method. After validation, <code>update()</code> calls <code>perform_update()</code> to save the changes.
<strong>Flow</strong>: <code>PUT Request</code> → <code>update()</code> → <code>perform_update()</code> → <code>serializer.save()</code></p>
</li>
<li><p>A <code>PATCH</code> request is routed to the <code>partial_update()</code> method. After partial validation, <code>partial_update()</code> also calls <code>perform_update()</code> to save the changes.
<strong>Flow</strong>: <code>PATCH Request</code> → <code>partial_update()</code> → <code>perform_update()</code> → <code>serializer.save()</code></p>
</li>
</ul>
<p><code>perform_update()</code> is the shared, final step for both. With that in mind, here’s when to override each one.</p>
<hr />
<h3>When to Override <code>perform_update(self, serializer)</code></h3>
<p>This is your go-to hook for 90% of update logic. Override <code>perform_update</code> when you have code that <strong>must run for any update, whether it's a</strong> <code>PUT</code> <strong>or a</strong> <code>PATCH</code>. It keeps your code DRY (Don't Repeat Yourself) by providing a single place for shared save-time logic.</p>
<ul>
<li><strong>Why Override?</strong> You need to stamp a field, clear a cache, or log an update without caring if it was a full or partial change.</li>
<li><strong>Example</strong>: You always want to record which user was the last person to modify an object.</li>
</ul>
<pre><code class="language-python">def perform_update(self, serializer):
    # This logic runs for both PUT and PATCH, after validation.
    # It's the cleanest place for shared save-time hooks.
    serializer.save(last_updated_by=self.request.user)
</code></pre>
<h3>When to Override <code>partial_update(self, request, *args, **kwargs)</code></h3>
<p>Override the <code>partial_update</code> method for logic that should <strong>only run during a partial change</strong> (<code>PATCH</code>). This is incredibly useful for implementing fine-grained permissions or triggering specific side effects when certain fields are modified</p>
<ul>
<li><strong>Why Override?</strong> You want to allow different update rules for different fields, or you want to react to specific, targeted changes.</li>
<li><strong>Example</strong>: In a project management system, maybe anyone can <code>PATCH</code> a task's <code>description</code>, but only a manager can <code>PATCH</code> its <code>due_date</code>. Overriding <code>partial_update</code> is the perfect place to check for this.</li>
</ul>
<pre><code class="language-python">def partial_update(self, request, *args, **kwargs):
    # Custom logic just for PATCH
    if 'due_date' in request.data and not request.user.is_manager:
        return Response({"error": "Only managers can change the due date."}, status=status.HTTP_403_FORBIDDEN)
    
    # Another example: trigger a notification ONLY if the status changes
    if 'status' in request.data:
        instance = self.get_object()
        old_status = instance.status
        new_status = request.data['status']
        if old_status != new_status:
            notify_team_of_status_change(instance, new_status)

    return super().partial_update(request, *args, **kwargs)
</code></pre>
<h3>When to Override <code>update(self, request, *args, **kwargs)</code></h3>
<p>Override the <code>update</code> method when you need to add logic that is <strong>specific to a full resource replacement</strong> (<code>PUT</code>). Because <code>PUT</code> implies replacing the entire resource, you might have preconditions or post-conditions that don't apply to a simple <code>PATCH</code>.</p>
<ul>
<li><strong>Why Override?</strong> You want to enforce a strict "all or nothing" replacement policy or perform an action that only makes sense when the entire object is changed.</li>
<li><strong>Example</strong>: Imagine you have a <code>Settings</code> object. You could override <code>update</code> to ensure that a <code>PUT</code> request truly contains every single required setting field, preventing users from accidentally wiping out settings by sending an incomplete object. You could also log a specific "Settings Overwritten" event that is more severe than a simple field change.</li>
</ul>
<pre><code class="language-python">def update(self, request, *args, **kwargs):
    # Custom logic just for PUT
    if not all(key in request.data for key in ['theme', 'notifications', 'timezone']):
        return Response({"error": "A full update requires all setting keys."}, status=status.HTTP_400_BAD_REQUEST)
    
    # Log the major change
    log_settings_overwritten(user=request.user)
    
    return super().update(request, *args, **kwargs)
</code></pre>
<hr />
<h2>Other Useful ViewSet Hooks</h2>
<p>Beyond <code>create</code> and <code>update</code>, other methods give you powerful control over the request lifecycle:</p>
<ul>
<li><p><code>list()</code>: Override this when you need to reshape the entire list response beyond a simple array of objects, for example, by adding metadata or aggregation summaries.</p>
</li>
<li><p><code>retrieve()</code>: Perfect for when a single object's response needs extra context that depends on the request, like adding a <code>can_edit</code> flag for the current user.</p>
</li>
<li><p><code>destroy()</code>: The default just deletes the object. Override this to implement soft deletes, check for dependencies, or record an audit log event.</p>
</li>
<li><p><code>get_queryset()</code>: This is one of the most important overrides for performance and security. It’s where you scope the data (e.g., a user only sees their own invoices) and where you should add performance boosters like <code>select_related</code> and <code>prefetch_related</code>.</p>
</li>
<li><p><code>get_serializer_class()</code>: Your viewset's "chameleon." It lets you use different serializers for different actions (e.g., a summary for <code>list</code>, details for <code>retrieve</code>).</p>
</li>
<li><p><code>get_permissions()</code>: Great for when permissions change based on the action (e.g., anyone can <code>GET</code>, but only staff can <code>POST</code>).</p>
</li>
</ul>
<hr />
<h2>The Decision Guide: Where Does This Logic Go?</h2>
<p>Let’s boil everything from both articles down to a simple cheat sheet for a clean DRF architecture:</p>
<ul>
<li><p><strong>Is it about the shape, structure, or validity of data?</strong></p>
<ul>
<li><strong>Answer</strong>: Serializer (<code>validate</code>, <code>validate_&lt;field&gt;</code>, <code>to_representation</code>).</li>
</ul>
</li>
<li><p><strong>Is it about managing the HTTP request/response flow, like returning a custom status code or handling action-specific permissions?</strong></p>
<ul>
<li><strong>Answer</strong>: ViewSet action (<code>create</code>, <code>update</code>, <code>list</code>, etc.).</li>
</ul>
</li>
<li><p><strong>Is it about changing how an object is saved after validation, for both <code>PUT</code> and <code>PATCH</code>?</strong></p>
<ul>
<li><strong>Answer</strong>: ViewSet <code>perform_*</code> method (<code>perform_create</code>, <code>perform_update</code>).</li>
</ul>
</li>
<li><p><strong>Is it about filtering the main list of objects based on the user or request?</strong></p>
<ul>
<li><strong>Answer</strong>: ViewSet <code>get_queryset</code> method.</li>
</ul>
</li>
</ul>
<p>Keeping these boundaries clear isn't just an abstract architectural exercise. It’s a practical strategy for building software that can grow and adapt without collapsing under its own weight. Your team will thank you. And more importantly, your future self will, too.</p>
]]></content:encoded></item><item><title><![CDATA[Part 1: Django REST Framework: When (and When Not) to Override Serializers and Viewsets]]></title><description><![CDATA[DRF, Part 1: Serializer Overrides
Ask any Django developer where to put validation, formatting, and orchestration logic, and you’ll often get different answers — some say “just put it in the serialize]]></description><link>https://soldatov-ss.hashnode.dev/part-1-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets</link><guid isPermaLink="true">https://soldatov-ss.hashnode.dev/part-1-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets</guid><category><![CDATA[Django]]></category><category><![CDATA[Python]]></category><category><![CDATA[architecture]]></category><category><![CDATA[clean code]]></category><dc:creator><![CDATA[Serhii Soldatov]]></dc:creator><pubDate>Sun, 10 Aug 2025 14:54:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/68e28793524135bc3bf4e0b1/36b35ec3-8444-41b0-8af4-39514956c4e0.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>DRF, Part 1: Serializer Overrides</h2>
<p>Ask any Django developer where to put validation, formatting, and orchestration logic, and you’ll often get different answers — some say “just put it in the serializer,” others drop everything in the viewset.</p>
<p>This works… until it doesn’t.
When your API grows, mixing concerns makes code brittle, hard to test, and painful to maintain. Knowing exactly <strong>when</strong> to override a serializer method and <strong>when</strong> to override a viewset method is key to keeping a codebase clean.</p>
<hr />
<h3>The Serializer's Core Mission</h3>
<p>Think of your serializer as the security guard at the door of your database. It's responsible for three primary tasks:</p>
<ol>
<li><strong>Validation</strong>: Checking the ID of incoming data to ensure it's legitimate.</li>
<li><strong>Transformation</strong>: Making sure the data is in the right format before being saved.</li>
<li><strong>Representation</strong>: Deciding how the data should be presented when it's sent back out.</li>
</ol>
<p>Its world is small and focused: just the data. Let's look at the tools it uses.</p>
<hr />
<h3>1) Serializer method overrides</h3>
<p><strong>Role</strong>
Serializers control what may enter and how it leaves: validation, input normalization, output formatting.</p>
<p><code>validate_&lt;field_name&gt;(self, value)</code> - field-level checks
This is your go-to for single-field validation. Django's model fields and DRF's serializer fields handle the basics, like checking if a field is an integer or a valid email. But what about your specific business rules?</p>
<p>Use this when a single field has a rule that only it needs to worry about.</p>
<p><strong>Example</strong>: Imagine you have a <code>Product</code> model with a <code>discount_percentage</code> field. For most products, any discount is fine. But for the "Electronics" category, you need to cap it at 50% to protect your margins.</p>
<p><code>validate(self, data)</code> - cross-field validation
Sometimes fields can't be validated in isolation. They have relationships; they depend on each other. <code>validate</code> is where your fields have a conversation. It runs after all the individual <code>validate_&lt;field&gt;</code> methods have passed.</p>
<p>Use this for cross-field validation.
<strong>Example</strong>: A classic case is a <code>PromoCampaign</code> model with a <code>start_date</code> and an <code>end_date</code>. It makes no sense for the promotion to end before it even begins.</p>
<pre><code class="language-python">def validate(self, data):
    """
    Check that the start date is before the end date.
    """
    if data['start_date'] &gt; data['end_date']:
        raise serializers.ValidationError("End date must occur after start date.")
    return data
</code></pre>
<p>Simple enough, right? This keeps your data integrity rules right next to the data definition.</p>
<p><code>create(self, validated_data)</code> 
DRF's default <code>create</code> method is wonderfully simple: it just unpacks your validated data and calls <code>YourModel.objects.create(**validated_data)</code>. But sometimes "simple" isn't enough.</p>
<ul>
<li>Override <code>create</code> when you need to control exactly how a new object instance comes into being. This could mean:</li>
<li>Creating related objects in the same transaction.</li>
<li>Injecting data that doesn't come from the user, like <code>created_by=self.context['request'].user</code>.</li>
<li>Handling nested serializers for write operations.</li>
</ul>
<pre><code class="language-python"># In a UserProfileSerializer that also creates a user
def create(self, validated_data):
    user_data = validated_data.pop('user')
    user = User.objects.create_user(**user_data)
    # Stamp the current user from the view's context
    created_by_user = self.context['request'].user
    profile = UserProfile.objects.create(user=user, created_by=created_by_user, **validated_data)
    return profile
</code></pre>
<p><code>update(self, instance, validated_data)</code>
Just like <code>create</code>, the default <code>update</code> method loops through the validated data and does a <code>setattr(instance, key, value)</code> for each item before calling <code>instance.save()</code>. You should override it when you have more complex update logic.</p>
<p>Maybe you need to prevent updates if an order is already "shipped." Or perhaps updating one field requires a calculated change to another.</p>
<p><strong>Example</strong>: When updating a blog post, you want to replace all its tags, not just add new ones. The default <code>update</code> for a many-to-many relationship might not do exactly what you want.</p>
<pre><code class="language-python"># In a PostSerializer with a 'tags' field
def update(self, instance, validated_data):
    tags_data = validated_data.pop('tags', None)

    # This is the default behavior for all other fields
    instance = super().update(instance, validated_data)

    # Now, handle the tags with our custom logic
    if tags_data is not None:
        instance.tags.set(tags_data) # Replaces all existing tags

    return instance
</code></pre>
<p><code>to_representation(self, instance)</code>
This is the "glow-up" method. It's the last stop before your data is serialized and sent out into the world. Its job is to shape the outbound data. The model might store a user ID, but you want to show their full name. The database has a <code>first_name</code> and <code>last_name</code>, but you want to add a <code>full_name</code> field to the API response.</p>
<p><strong>Example</strong>: Add a computed field to your User serializer.</p>
<pre><code class="language-python"># In your UserSerializer
def to_representation(self, instance):
    # Get the default representation
    representation = super().to_representation(instance)
    # Add our custom field
    representation['full_name'] = instance.get_full_name()
    return representation
</code></pre>
<h3>So, When Should Serializers Just Say No?</h3>
<p>This is just as important. A serializer that does too much becomes a god object. Here’s what doesn't belong in a serializer:</p>
<p><strong>Heavy Side Effects</strong>: Sending emails, charging credit cards, updating inventory in another system, calling third-party APIs. This is a one-way ticket to a maintenance nightmare. If the database transaction fails after the email is sent, what do you do? This logic belongs elsewhere.</p>
<p><strong>Complex Queries</strong>: A serializer's <code>validate</code> method shouldn't be making five different database calls to check a condition. That logic should live in a dedicated place (like a "selector" function or repository's function) and be called from the view.</p>
<p><strong>Business Workflows</strong>: A multi-step process, like "register user, create trial subscription, and schedule a welcome email," is a business workflow. It's too high-level for a serializer. This is a job for a service layer.</p>
<hr />
<p>In <a href="https://dev.to/soldatov-ss/part-2-django-rest-framework-when-and-when-not-to-override-serializers-and-viewsets-3j32">Part 2</a> of this series, we’ll dive into the ViewSet's role as the orchestra's conductor and explore how to use its methods—and a service layer—to keep your application logic clean and organized.</p>
]]></content:encoded></item><item><title><![CDATA[Why Django REST Framework doesn't show your custom validation error messages (and what to do about it)]]></title><description><![CDATA[The GitHub Discussion: link

Issue developers face:
When developing with Django REST Framework (DRF), developers typically run into issues where error messages for custom validation specified on Django model constraints (e.g., UniqueConstraint.violat...]]></description><link>https://soldatov-ss.hashnode.dev/why-django-rest-framework-doesnt-show-your-custom-validation-error-messages-and-what-to-do-about-it</link><guid isPermaLink="true">https://soldatov-ss.hashnode.dev/why-django-rest-framework-doesnt-show-your-custom-validation-error-messages-and-what-to-do-about-it</guid><dc:creator><![CDATA[Serhii Soldatov]]></dc:creator><pubDate>Wed, 21 May 2025 12:52:56 GMT</pubDate><content:encoded><![CDATA[<p>The GitHub Discussion: <a target="_blank" href="https://github.com/encode/django-rest-framework/discussions/7850">link</a></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759677232635/829030d0-47ed-450e-a322-b0303b667170.png" alt="Image description" /></p>
<h3 id="heading-issue-developers-face">Issue developers face:</h3>
<p>When developing with Django REST Framework (DRF), developers typically run into issues where error messages for custom validation specified on Django model constraints (e.g., UniqueConstraint.violation_error_message) are not passed through to API responses. DRF returns default validation error messages instead.</p>
<p>For example, a custom error message like:</p>
<pre><code class="lang-python">UniqueConstraint(
    fields=[<span class="hljs-string">'email'</span>, <span class="hljs-string">'username'</span>],
    violation_error_message=<span class="hljs-string">'This email and username combination already exists.'</span>
)
</code></pre>
<p>...might never actually reach the API client.</p>
<p>Instead, you'll see something generic like:</p>
<pre><code class="lang-python">{<span class="hljs-string">"non_field_errors"</span>: [<span class="hljs-string">"The fields email, username must make a unique set."</span>]}
</code></pre>
<p>This is what the DRF maintainers had to say about it:</p>
<ul>
<li><p>DRF validation is deliberately occurring at the serializer level, <strong>before</strong> the creation of the model instance.</p>
</li>
<li><p>It intentionally avoids calling Django's <code>full_clean()</code> in this processes, as serializers are meant to validate incoming data instead of an already-created model instance.</p>
</li>
<li><p>Due to the complexity and backward compatibility issues, there are no immediate plans from DRF for changing this behavior.</p>
</li>
</ul>
<p>In other words - DRF prioritises serialiser checking for API input, which means that unless you explicitly call model checking (e.g. via <code>full_clean()</code>), constraint errors and their custom messages will never be called or shown in API responses.</p>
<h3 id="heading-possible-solutions">Possible solutions:</h3>
<p>Developers can tackle this issue through several practical approaches:</p>
<ul>
<li><p><strong>Explicitly calling <code>full_clean()</code>:</strong></p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">create</span>(<span class="hljs-params">self, validated_data</span>):</span>
  instance = MyModel(**validated_data)
  <span class="hljs-keyword">try</span>:
      instance.full_clean()
  <span class="hljs-keyword">except</span> DjangoValidationError <span class="hljs-keyword">as</span> e:
      <span class="hljs-keyword">raise</span> DRFValidationError(e.message_dict)
  instance.save()
  <span class="hljs-keyword">return</span> instance
</code></pre>
</li>
<li><p><strong>Custom serializer validators:</strong></p>
</li>
</ul>
<p>This method just overrides DRF's built-in UniqueTogetherValidator just to replace the error message.</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CustomUniqueValidator</span>(<span class="hljs-params">UniqueTogetherValidator</span>):</span>
    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__call__</span>(<span class="hljs-params">self, attrs, serializer</span>):</span>
        <span class="hljs-keyword">try</span>:
            super().__call__(attrs, serializer)
        <span class="hljs-keyword">except</span> serializers.ValidationError:
            <span class="hljs-keyword">raise</span> serializers.ValidationError(<span class="hljs-string">"This email and username combination already exists."</span>)
</code></pre>
<ul>
<li><strong>Sometimes just double your validation code:</strong>
Sometimes, custom validators and extra abstractions aren't needed. Although we all know the DRY principle, sometimes it's acceptable to just dublicate simple validation logicdirectly into your serializer</li>
</ul>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyModelSerializer</span>(<span class="hljs-params">serializers.ModelSerializer</span>):</span>
    <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Meta</span>:</span>
        model = MyModel
        fields = [<span class="hljs-string">'email'</span>, <span class="hljs-string">'username'</span>]
        validators = [
            UniqueTogetherValidator(
                queryset=MyModel.objects.all(),
                fields=[<span class="hljs-string">'email'</span>, <span class="hljs-string">'username'</span>],
                message=<span class="hljs-string">"This email and username combination already exists."</span>
            )
        ]
</code></pre>
<h4 id="heading-summary">Summary:</h4>
<p><strong>Issue</strong>: DRF doesn't carry forward any custom validation messages defined in Django.</p>
<p><strong>Reason</strong>: For reasons of architecture and backward compatibility, DRF validates its data independently from Django's model validation.</p>
<h4 id="heading-important-warnings">Important Warnings:</h4>
<p>Be careful where you place validation logic. Django models can be modified via the admin interface, serializers, or directly with bulk operations (.update()).</p>
<p>Placing the critical validation logic into <code>save()</code> or <code>full_clean()</code> is problematic, as those aren't called when performing bulk updates or certain direct model manipulations. Always bear these edge cases in mind when developing your validation plans.</p>
<p>This approach keeps your validation logic consistent, reusable, and well-documented throughout your project.</p>
]]></content:encoded></item><item><title><![CDATA[Boost Your Django Docker Images by using UV package and project manager]]></title><description><![CDATA[What's UV?
UV is an ultra-fast Python package manager written in Rust by the Astral team. You might already be familiar with another great product from Astral - ruff (popular Python linter).
It drastically reduces dependency installation time and pro...]]></description><link>https://soldatov-ss.hashnode.dev/boost-your-django-docker-images-by-using-uv-package-and-project-manager</link><guid isPermaLink="true">https://soldatov-ss.hashnode.dev/boost-your-django-docker-images-by-using-uv-package-and-project-manager</guid><category><![CDATA[UV ]]></category><category><![CDATA[Python]]></category><category><![CDATA[Docker]]></category><category><![CDATA[Django]]></category><category><![CDATA[package manager]]></category><dc:creator><![CDATA[Serhii Soldatov]]></dc:creator><pubDate>Sun, 30 Mar 2025 14:55:56 GMT</pubDate><content:encoded><![CDATA[<h2 id="heading-whats-uv">What's UV?</h2>
<p><strong>UV</strong> is an ultra-fast Python package manager written in Rust by the <a target="_blank" href="https://github.com/astral-sh">Astral team</a>. You might already be familiar with another great product from Astral - <a target="_blank" href="https://github.com/astral-sh/ruff">ruff</a> <em>(popular Python linter)</em>.</p>
<p>It drastically reduces dependency installation time and produces smaller, optimized Docker images—ideal for enhancing your Django project's Docker workflow. Let's dive in!</p>
<h2 id="heading-quick-start">Quick start:</h2>
<p>Follow these simple steps to get your local environment ready:</p>
<pre><code class="lang-plaintext">uv venv    # 1. Create a UV Virtual Environment
source .venv/bin/activate    # 2. Activate the Virtual Environment
uv sync --all-groups    # 3. Install Project Dependencies
docker-compose up --build
</code></pre>
<p><strong>Note:</strong> This installs both <strong>production</strong> and <strong>development</strong> dependencies. Use <code>uv sync --no-dev</code> if you want <strong>production-only</strong> packages.</p>
<h2 id="heading-dockerfile">Dockerfile</h2>
<p>Here's a breakdown of a Dockerfile designed specifically for Django applications using UV to manage Python dependencies:</p>
<pre><code class="lang-plaintext">FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS base
FROM base AS builder

# Set up environment
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy

WORKDIR /app

# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project --all-groups

# Then, add the rest of the project source code and install it
# Installing separately from its dependencies allows optimal layer caching
COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --all-groups

FROM base
COPY --from=builder /app /app
ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000

CMD ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
</code></pre>
<p>##Base Image We start by leveraging a slim Python image <strong>with UV pre-installed</strong>, optimized for minimal size and maximum speed.</p>
<pre><code class="lang-plaintext">FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS base
FROM base AS builder
</code></pre>
<p>##Environment Setup <code>PYTHONDONTWRITEBYTECODE</code> prevents Python from writing .pyc files to disk.</p>
<p><code>PYTHONUNBUFFERED</code> ensures immediate logging output.</p>
<p><code>UV_COMPILE_BYTECODE</code> compiles Python bytecode for faster startup.</p>
<p><code>UV_LINK_MODE=copy</code> configures UV to copy dependencies instead of symlinking (improving compatibility).</p>
<p><code>UV_PROJECT_ENVIRONMENT</code> sets the virtual environment path.</p>
<p><strong>!Permission denied issue</strong>: You might encounter permission issues when using the default virtual environment paths provided by UV as and I faced:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759677237091/919822ae-0040-4461-8317-14b78ca660c9.png" alt="PermissionDeniedImage" /></p>
<p>You can handle this issue in two ways:</p>
<ol>
<li><em>(Recommended)</em> By adding second volume entry(<code>/app/.venv</code>) in your docker-compose file. Since the <code>.venv</code> directory in the container is now completely isolated from your host filesystem and Docker can't change ownership or permissions on your local <code>.venv</code> folder, which solves your permission issues.</li>
</ol>
<p>2.By explicitly setting <code>UV_PROJECT_ENVIRONMENT</code> to a dedicated path (/app/venv), we create a clean separation from default or locally-created environments, effectively resolving permission conflicts.</p>
<p>##Dependency Installation (Optimized Caching) <em>This example is adapted from the official UV example:</em> <a target="_blank" href="https://github.com/astral-sh/uv-docker-example/blob/main/Dockerfile"><em>Dockerfile</em></a></p>
<pre><code class="lang-plaintext">RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    uv sync --frozen --no-install-project --all-groups
</code></pre>
<p>UV installs dependencies from <code>pyproject.toml</code> and <code>uv.lock</code>.</p>
<p>⚠️ <strong>Important:</strong> Here, I've used <code>--all-groups</code> to install every dependency group defined in <code>pyproject.toml</code> (including development dependencies like linting tools). For production builds, consider switching to <code>--no-dev</code> to install only production dependencies. For detailed guidance on managing dependency groups, refer to the official Dependency <a target="_blank" href="https://docs.astral.sh/uv/concepts/projects/dependencies/#dependency-groups">Groups documentation</a>.</p>
<p>##Adding Project Source and Finalizing Installation</p>
<pre><code class="lang-plaintext">COPY . /app
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --all-groups
</code></pre>
<p><strong>!Note:</strong> I've also used <code>--all-groups</code> in there. By adding your project's source after installing dependencies, Docker efficiently caches layers, speeding up rebuilds when code changes occur.</p>
<p>##Final Runtime Stage</p>
<pre><code class="lang-plaintext">FROM base
COPY --from=builder /app /app
ENV PATH="/app/venv/bin:$PATH"
EXPOSE 8000

CMD ["uv", "run", "python", "manage.py", "runserver", "0.0.0.0:8000"]
</code></pre>
<p>In this final image: ✅ Only the necessary files and pre-installed dependencies are copied from the builder stage. ✅ Django runs within UV's isolated virtual environment.</p>
<p>⚠️ <strong>Important Considerations:</strong></p>
<ul>
<li><p><strong>Production Use</strong>: This setup uses Django’s built-in development server (<code>runserver</code>). For production environments, consider switching to a production-grade server like <code>gunicorn</code> or <code>uvicorn</code>.</p>
</li>
<li><p><strong>Running Commands with UV</strong>: After configuring your virtual environment with UV, you <strong>MUST</strong> prefix your commands with <code>uv run</code>, e.g.:</p>
</li>
</ul>
<pre><code class="lang-plaintext">uv run python manage.py ...
uv run pytest -s -v
</code></pre>
<p>Otherwise, commands executed outside UV's context will fail.</p>
<p>Here's how can look like your <code>start.sh</code>:</p>
<pre><code class="lang-plaintext">#!/usr/bin/env bash

set -o errexit
set -o pipefail
set -o nounset
set -o xtrace

uv run wait_for_postgres.py

uv run manage.py migrate
uv run manage.py runserver 0.0.0.0:8000
</code></pre>
<p>Your optimized Dockerfile is now ready, resulting in faster builds, smaller images, and better caching strategies for your Django application.</p>
<p>Here's quick time comparison:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759677238257/08bfe782-9281-4d02-87a9-fed32e386cf3.png" alt="New docker file" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759677239684/c013af00-0ee0-4813-875c-2d271c518f51.png" alt="Old docker file" /></p>
<p>With the optimized Dockerfile, the build process completed in just 15.8s, with only 6.7s dedicated to installing dependencies. In contrast, the previous Dockerfile took 34.6s, spending 28.3s installing dependencies.</p>
<p>You can easily measure this improvement yourself by running:</p>
<pre><code class="lang-plaintext">time docker build -t container-name . --no-cache
</code></pre>
<p>And finally, here's an example of <code>pyproject.toml</code> and <code>docker-compose.yml</code> part:</p>
<pre><code class="lang-plaintext">[project]
name = "piedpiper-web"
version = "0.1.0"
description = "Piedpiper web"
readme = "README.md"
requires-python = "&gt;=3.13"

# Core application dependencies
dependencies = [
    "boto3~=1.37",
    "dj-database-url==2.3.0",
    "dj-rest-auth==7.0.1",
    "django==5.1.7",
    "django-allauth==65.3.0",
    "django-autoslug==1.9.9",
    "django-configurations==2.5.1",
    "django-cors-headers==4.7.0",
    "django-filter==25.1",
    "django-model-utils==5.0.0",
    "django-role-permissions==3.2.0",
    "django-storages==1.14.5",
    "django-unique-upload==0.2.1",
    "djangorestframework==3.15.2",
    "djangorestframework-api-key==3.0.0",
    "djangorestframework-simplejwt==5.5.0",
    "gunicorn==23.0.0",
    "pillow~=11.1",
    "psycopg2-binary==2.9.10",
    "python-dotenv==1.0.1",
    "requests==2.32.3",
    "setuptools==77.0.3",
]

[dependency-groups]
# Linting and code quality dependencies
lint = [
    "black==25.1.0",
    "flake8==7.1.2",
    "isort==6.0.1",
    "pre-commit==4.2.0",
    "ruff==0.11.2",
]

# dev dependencies
dev = [
    "django-silk&gt;=5.3.2",
    "nplusone&gt;=1.0.0",
    "ipdb==0.13.13",
    "ipython==8.34.0",
    "mock==5.2.0",
    "coverage~=7.7",
    "pytest-django==4.10.0",
    "factory-boy==3.3.3"
    "drf-yasg~=1.21",
]
</code></pre>
<p><code>docker-compose</code>:</p>
<pre><code class="lang-plaintext">  web:
    restart: always
    build:
      context: .
      dockerfile: Dockerfile
      target: builder
    command: bash scripts/start.sh
    working_dir: /app
    volumes:
      - ./:/app
      - /app/.venv
    ports:
      - "8000:8000"
    depends_on:
      - postgres
    env_file:
      - .env
</code></pre>
<h2 id="heading-more-resourses">More resourses:</h2>
<p><a target="_blank" href="https://docs.astral.sh/uv/">Official documentation</a><a target="_blank" href="https://www.saaspegasus.com/guides/uv-deep-dive/">uv: An In-Depth Guide to Python's Fast and Ambitious New Package Manager</a><a target="_blank" href="https://depot.dev/docs/container-builds/how-to-guides/optimal-dockerfiles/python-uv-dockerfile">Best practice Dockerfile for Python with uv</a></p>
<p>Hope it was useful, Thanks for reading 🙌</p>
]]></content:encoded></item></channel></rss>