<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: doo</title>
    <description>The latest articles on DEV Community by doo (@mr-doosun).</description>
    <link>https://dev.to/mr-doosun</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3845436%2F481cf630-6f72-4d83-ad31-2a4438121aaa.png</url>
      <title>DEV Community: doo</title>
      <link>https://dev.to/mr-doosun</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mr-doosun"/>
    <language>en</language>
    <item>
      <title>I got mass of CRUD in my FastAPI project, so I fixed it</title>
      <dc:creator>doo</dc:creator>
      <pubDate>Sat, 28 Mar 2026 18:07:32 +0000</pubDate>
      <link>https://dev.to/mr-doosun/i-got-mass-of-crud-in-my-fastapi-project-so-i-fixed-it-3fl0</link>
      <guid>https://dev.to/mr-doosun/i-got-mass-of-crud-in-my-fastapi-project-so-i-fixed-it-3fl0</guid>
      <description>&lt;p&gt;I've been working on a FastAPI project that grew to about 10 domains. At some point I realized I wasn't really building features anymore. I was just copying the same repository-service-router stack into a new folder, changing the model name, and praying I didn't forget to wire something up.&lt;/p&gt;

&lt;p&gt;The worst part was code reviews. Every developer on the team structured things slightly differently. One person puts DTOs in the service layer, another puts them in the router. Someone imports a repository directly from another domain. Nobody notices until it's already merged and tangled.&lt;/p&gt;

&lt;p&gt;I kept thinking there has to be a better way to do this.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually repeating
&lt;/h2&gt;

&lt;p&gt;Every single domain needed the same thing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@router.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserCreate&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_db&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;new_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;new_user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I'd write the same thing for products. And orders. And payments. Seven CRUD operations each, all nearly identical except for the model name.&lt;/p&gt;

&lt;p&gt;I tried a few approaches. Code generators felt brittle — the generated code drifts from the template over time and you end up maintaining two things. Mixins got messy fast. What actually worked was plain Python generics.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I ended up building
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseRepository&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProductDTO&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ProductModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;return_entity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ProductDTO&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseService&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProductDTO&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_repository&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ProductRepositoryProtocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;product_repository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the entire setup for a new domain's CRUD. Seven async methods come from the base classes. If I need custom logic, I just override the specific method. The rest stays as-is.&lt;/p&gt;

&lt;p&gt;No code generation, no magic. It's just inheritance with generics. Your IDE still understands everything, types flow through, and you can cmd+click into any method.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other thing that was driving me crazy
&lt;/h2&gt;

&lt;p&gt;Every time I added a new domain, I had to go edit the DI container, update the app bootstrap, register the router... it was like 4-5 files of boilerplate changes just to say "hey, this domain exists now."&lt;/p&gt;

&lt;p&gt;So I wrote a &lt;code&gt;discover_domains()&lt;/code&gt; function. If a folder has an &lt;code&gt;__init__.py&lt;/code&gt; and a DI container in the right place, it gets picked up automatically. No manual registration.&lt;/p&gt;

&lt;p&gt;This sounds small but it removed so much friction. New developer wants to add a domain? Just create the folder structure and it works. No need to understand the entire app's wiring first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping the architecture from rotting
&lt;/h2&gt;

&lt;p&gt;The other problem with a growing team is architecture erosion. Someone imports a repository from the domain layer. Someone else calls a service directly from another domain's infrastructure. These tiny violations add up until the dependency graph is a mess.&lt;/p&gt;

&lt;p&gt;I added pre-commit hooks that check import boundaries. If your domain layer tries to import from infrastructure, the commit gets rejected. It felt aggressive at first, but honestly it's saved us from so many "how did this dependency get here?" debugging sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  One unexpected thing
&lt;/h2&gt;

&lt;p&gt;I started using Claude Code with this project and ended up building slash commands for common tasks. &lt;code&gt;/new-domain product&lt;/code&gt; scaffolds all the files. &lt;code&gt;/review-architecture&lt;/code&gt; checks if anything violates the layer rules. There are about 12 of these now.&lt;/p&gt;

&lt;p&gt;The one that surprised me most was &lt;code&gt;/onboard&lt;/code&gt;. New developers run it and it walks them through the project structure interactively, adapting to their experience level. It's not essential but it cut onboarding time significantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;p&gt;I probably over-engineered the DI container setup early on. &lt;code&gt;dependency-injector&lt;/code&gt; is powerful but has a learning curve. If I started over, I might keep it simpler for the first few domains and introduce it later.&lt;/p&gt;

&lt;p&gt;Also, I spent too long before writing ADRs (Architecture Decision Records). Once I started documenting why I chose Taskiq over Celery, or why I switched from Poetry to UV, it became so much easier to onboard people and settle debates.&lt;/p&gt;

&lt;h2&gt;
  
  
  The repo
&lt;/h2&gt;

&lt;p&gt;I open-sourced it here if anyone wants to take a look: &lt;a href="https://github.com/Mr-DooSun/fastapi-blueprint" rel="noopener noreferrer"&gt;github.com/Mr-DooSun/fastapi-blueprint&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's been working well for our team but I'm curious how others handle this. If you're managing a FastAPI project with a lot of domains, what patterns have you settled on? Especially interested in how people handle the "same CRUD, different model" problem.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>fastapi</category>
      <category>python</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
