<?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>Forem: Marcos Filipe Capella</title>
    <description>The latest articles on Forem by Marcos Filipe Capella (@marcoscapella).</description>
    <link>https://forem.com/marcoscapella</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%2F884244%2F98e798c1-4f1f-4514-b57e-728a983bcbab.PNG</url>
      <title>Forem: Marcos Filipe Capella</title>
      <link>https://forem.com/marcoscapella</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/marcoscapella"/>
    <language>en</language>
    <item>
      <title>CRUD - An honest copy-paste recipe to use guilt-free</title>
      <dc:creator>Marcos Filipe Capella</dc:creator>
      <pubDate>Sun, 08 Feb 2026 15:08:32 +0000</pubDate>
      <link>https://forem.com/marcoscapella/crud-an-honest-copy-paste-recipe-to-use-guilt-free-4db6</link>
      <guid>https://forem.com/marcoscapella/crud-an-honest-copy-paste-recipe-to-use-guilt-free-4db6</guid>
      <description>&lt;p&gt;My days as a Programming TA at the Catholic University of Pernambuco are over, but I still have that drive to help out &lt;strong&gt;peer-to-peer&lt;/strong&gt;. Lately, I’ve seen a lot of talk out there claiming that junior developers can’t write a CRUD without generative AI. I find that a bit sensationalist because the "recipe" is simple and applies to almost every language. #NoGateKeeping&lt;/p&gt;

&lt;p&gt;This recipe is something I keep in my notebooks and update as I learn. The first version is from 2023, back when I was taking Object-Oriented Programming and studying SOLID. I’ve adapted it as new projects came along, and this is the result.&lt;/p&gt;

&lt;p&gt;Ready to Ctrl+&lt;/p&gt;

&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;The repository is the layer in contact with the &lt;strong&gt;Database&lt;/strong&gt;. Depending on the framework or language, this will change quite a bit, but in general, we should have these methods:&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="c1"&gt;# REPOSITORY LAYER (Abstract)
&lt;/span&gt;
&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entity_with_id&lt;/span&gt;
&lt;span class="nf"&gt;find_by_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;entity&lt;/span&gt;
&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;
&lt;span class="nf"&gt;find_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;entity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Service
&lt;/h3&gt;

&lt;p&gt;The Service layer is where the CRUD itself lives. &lt;strong&gt;Kent Beck&lt;/strong&gt; suggests that as a best practice, developers should write code that meets reading expectations and follows conventions. Not straying from the expected is essential. I always make sure to write in the &lt;strong&gt;C-R-U-D&lt;/strong&gt; order.&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="c1"&gt;# SERVICE LAYER
&lt;/span&gt;
&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attr1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attr2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attr3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Guard clauses - business logic validations
&lt;/span&gt;    &lt;span class="c1"&gt;# 2) [Optional] Check for duplicates/business rules via repository
&lt;/span&gt;    &lt;span class="c1"&gt;# 3) Create entity/domain object
&lt;/span&gt;    &lt;span class="c1"&gt;# 4) Call repository.save(entity)
&lt;/span&gt;    &lt;span class="c1"&gt;# 5) Return DTO or created ID
&lt;/span&gt;
&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Guard clause - validate ID
&lt;/span&gt;    &lt;span class="c1"&gt;# 2) Call repository.find_by_id(id)
&lt;/span&gt;    &lt;span class="c1"&gt;# 3) Guard clause - check if found (raise NotFound if not)
&lt;/span&gt;    &lt;span class="c1"&gt;# 4) Return entity or DTO
&lt;/span&gt;
&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;update_attributes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Guard clauses - validate ID and attributes
&lt;/span&gt;    &lt;span class="c1"&gt;# 2) Call repository.find_by_id(id)
&lt;/span&gt;    &lt;span class="c1"&gt;# 3) Guard clause - check if found
&lt;/span&gt;    &lt;span class="c1"&gt;# 4) Apply changes to the entity (using entity methods, not direct setters)
&lt;/span&gt;    &lt;span class="c1"&gt;# 5) [Optional] Post-update business validations
&lt;/span&gt;    &lt;span class="c1"&gt;# 6) Call repository.update(entity) or save(entity) - depends on the framework
&lt;/span&gt;    &lt;span class="c1"&gt;# 7) Return updated entity or success response
&lt;/span&gt;
&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Guard clause - validate ID
&lt;/span&gt;    &lt;span class="c1"&gt;# 2) [Optional] Check existence via repository.exists(id)
&lt;/span&gt;    &lt;span class="c1"&gt;# 3) [Optional] Business validations (Can it be deleted? Does it have dependencies?)
&lt;/span&gt;    &lt;span class="c1"&gt;# 4) Call repository.delete(id)
&lt;/span&gt;    &lt;span class="c1"&gt;# 5) Return success response
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In the create and update methods, I like returning the entity because it saves the front-end from having to rebuild the object with a new call, but there are valid criticisms of this—which is why I noted "OR". A service can also have other CRUD methods, like a "read" using an attribute other than the ID (e.g., searching for a user by email).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Controller
&lt;/h3&gt;

&lt;p&gt;These are our &lt;strong&gt;endpoints&lt;/strong&gt;! Did you get the flow so far? The controller builds the endpoint methods; it's the external layer. It calls the service, which performs the CRUD operations by calling methods from the repository (the innermost layer closest to the Database).&lt;/p&gt;

&lt;p&gt;The controller handles &lt;strong&gt;HTTP requests and exceptions&lt;/strong&gt;. Personally, I check if I’m following the &lt;strong&gt;Single Responsibility Principle (SRP)&lt;/strong&gt; just by looking at the imports. If I see an HTTP library being called in the service, I know I’ve messed up.&lt;/p&gt;

&lt;p&gt;Below, I’m using &lt;strong&gt;FastAPI&lt;/strong&gt; as the standard, which is what I work with today in both my job and research, but this recipe was originally created when I was primarily coding in &lt;strong&gt;Java Spring Boot&lt;/strong&gt;.&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="c1"&gt;# CONTROLLER LAYER (FastAPI)
&lt;/span&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;/resource&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;201&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_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;CreateResourceDTO&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) [Optional] Extra input validations (Pydantic already does a lot)
&lt;/span&gt;    &lt;span class="c1"&gt;# 2) Call service.create(request.attr1, request.attr2, ...)
&lt;/span&gt;    &lt;span class="c1"&gt;# 3) [Exception Handling] Catch business errors and convert to HTTP
&lt;/span&gt;    &lt;span class="c1"&gt;# 4) Return formatted response (201 Created + Location header if pure REST)
&lt;/span&gt;
&lt;span class="nd"&gt;@router.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/resource/{id}&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;get_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Call service.read(id)
&lt;/span&gt;    &lt;span class="c1"&gt;# 2) [Exception Handling] NotFound -&amp;gt; 404, etc.
&lt;/span&gt;    &lt;span class="c1"&gt;# 3) Return formatted response (200 OK + entity)
&lt;/span&gt;
&lt;span class="nd"&gt;@router.put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/resource/{id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# or PATCH for partial replacement
&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;update_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UpdateResourceDTO&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Call service.update(id, request)
&lt;/span&gt;    &lt;span class="c1"&gt;# 2) [Exception Handling]
&lt;/span&gt;    &lt;span class="c1"&gt;# 3) Return formatted response (200 OK + updated entity)
&lt;/span&gt;
&lt;span class="nd"&gt;@router.delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/resource/{id}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;204&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;delete_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 1) Call service.delete(id)
&lt;/span&gt;    &lt;span class="c1"&gt;# 2) [Exception Handling]
&lt;/span&gt;    &lt;span class="c1"&gt;# 3) Return 204 No Content (no body)
&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;So, here is my honest, "straight-from-the-heart" recipe to use for life. Ready for the &lt;strong&gt;Ctrl+V&lt;/strong&gt;?&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;This article was originally &lt;a href="https://www.linkedin.com/pulse/crud-receita-sincera-para-copiar-e-colar-usar-sem-culpa-capella-gvrhf/?trackingId=ZRkFNTa3TySKa0qwe1%2FwUA%3D%3D" rel="noopener noreferrer"&gt;posted by me in Portuguese on my LinkedIn account&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
