<?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: A0mineTV</title>
    <description>The latest articles on DEV Community by A0mineTV (@blamsa0mine).</description>
    <link>https://dev.to/blamsa0mine</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2455723%2Fdd380ab3-92f1-4067-a1b9-16b49c6b5a13.jpg</url>
      <title>DEV Community: A0mineTV</title>
      <link>https://dev.to/blamsa0mine</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/blamsa0mine"/>
    <language>en</language>
    <item>
      <title>One interface, eight job sources: designing a connector pipeline that doesn't fight you</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Tue, 15 Sep 2026 13:36:12 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/one-interface-eight-job-sources-designing-a-connector-pipeline-that-doesnt-fight-you-21db</link>
      <guid>https://dev.to/blamsa0mine/one-interface-eight-job-sources-designing-a-connector-pipeline-that-doesnt-fight-you-21db</guid>
      <description>&lt;p&gt;I built DevRadar as a personal dashboard to track developer job offers across the French market. Nothing fancy — a Laravel app, a few dozen offers a day, one user (me). But it pulls from eight very different sources: a government API with OAuth2, two HTML pages I scrape by hand, a couple of official REST APIs, an RSS feed, and — most recently — a mailbox full of job-alert emails.&lt;/p&gt;

&lt;p&gt;Those sources have almost nothing in common at the transport level. What they &lt;em&gt;do&lt;/em&gt; have in common is what happens after the data arrives: turn it into a job offer, figure out if we've seen it before, save it, keep a record of the sync. That's the part worth designing well, because it's the part every future source has to go through.&lt;/p&gt;

&lt;p&gt;This post walks through how that pipeline is built — two files, &lt;code&gt;SynchronizeSourceAction&lt;/code&gt; and &lt;code&gt;PersistOfferAction&lt;/code&gt;, plus the small set of types that connect them — and why the design paid for itself the day I added a source that isn't even a network request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the problem
&lt;/h2&gt;

&lt;p&gt;Here's the connector list as it stands in &lt;code&gt;docs/job-sources.md&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Transport&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;France Travail&lt;/td&gt;
&lt;td&gt;REST API, OAuth2 client credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Adzuna&lt;/td&gt;
&lt;td&gt;REST API, app key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free-Work&lt;/td&gt;
&lt;td&gt;HTML listing page → JSON-LD on detail pages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HelloWork&lt;/td&gt;
&lt;td&gt;HTML "métier" pages → JSON-LD + an inline analytics blob&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WeLoveDevs&lt;/td&gt;
&lt;td&gt;HTML page with a full Algolia search response embedded server-side&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;We Work Remotely&lt;/td&gt;
&lt;td&gt;RSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jooble&lt;/td&gt;
&lt;td&gt;IMAP — reading the user's own job-alert emails&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of these needs different auth, different pagination, different error modes, different parsing. A malformed HTML page, an empty RSS feed, and an IMAP connection timeout are not remotely the same failure — but by the time the data reaches persistence, none of that should matter anymore.&lt;/p&gt;

&lt;p&gt;So the pipeline draws one hard line: &lt;strong&gt;a connector's only job is to produce a list of &lt;code&gt;NormalizedOffer&lt;/code&gt; DTOs.&lt;/strong&gt; Everything before that line is source-specific and lives in &lt;code&gt;app/Sync/Connectors/&lt;/code&gt;. Everything after it is source-agnostic and lives in &lt;code&gt;app/Actions/Sync/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The contract
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;Connector&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @throws InvalidConnectorConfigurationException */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;validateConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Source&lt;/span&gt; &lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="cd"&gt;/** @throws ConnectorFetchException */&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Source&lt;/span&gt; &lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ConnectorPage&lt;/span&gt;&lt;span class="p"&gt;;&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 whole interface. &lt;code&gt;fetch()&lt;/code&gt; returns a &lt;code&gt;ConnectorPage&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ConnectorPage&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$offers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// list&amp;lt;NormalizedOffer&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$nextCursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$hasMorePages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A source with no real pagination (most of the HTML ones) just always returns &lt;code&gt;hasMorePages: false&lt;/code&gt;. A source with cursor-based pagination (the APIs) hands back whatever token means "continue from here" — a page number, an offset, an API-specific cursor string. The orchestrator doesn't care which; it just keeps asking until &lt;code&gt;hasMorePages&lt;/code&gt; is false.&lt;/p&gt;

&lt;p&gt;And every offer that comes out the other end, regardless of source, is the same shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NormalizedOffer&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$externalId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$company&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$contractType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// raw string — mapped later&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$remoteStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// raw string — mapped later&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?int&lt;/span&gt; &lt;span class="nv"&gt;$salaryMinimum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$technologies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?CarbonImmutable&lt;/span&gt; &lt;span class="nv"&gt;$publishedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$rawPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$evidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;?float&lt;/span&gt; &lt;span class="nv"&gt;$extractionConfidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth calling out, because they're the two rules that keep eight connectors from turning into eight slightly-different dialects:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every field is nullable, and a connector must leave it null when the source doesn't provide it — never guess.&lt;/strong&gt; If Jooble's alert email doesn't give me a job description, &lt;code&gt;description&lt;/code&gt; stays &lt;code&gt;null&lt;/code&gt;. It doesn't get padded with the title, it doesn't get inferred from the company name. An empty field is honest data; a guessed field is a bug waiting to surface three steps downstream.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Contract type and remote policy are passed as raw strings, not enums.&lt;/strong&gt; A connector doesn't decide whether "CDI" or "temps plein" means &lt;code&gt;ContractType::Cdi&lt;/code&gt; — it just hands over whatever text the source gave it, and a shared mapper does the classification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OfferFieldMapper&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;mapContractType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$raw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?ContractType&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$needle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$raw&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// fold accents, lowercase, collapse whitespace&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$needle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cdi'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$needle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'temps plein'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ContractType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Cdi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$needle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'freelance'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$needle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'independant'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ContractType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Freelance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$needle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'alternance'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;str_contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$needle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'apprentissage'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;ContractType&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Alternance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="c1"&gt;// ...&lt;/span&gt;
            &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This one function is why an HTML connector scraping French badges ("✅ CDI") and a REST API returning &lt;code&gt;employmentType: FULL_TIME&lt;/code&gt; both end up with the same &lt;code&gt;ContractType::Cdi&lt;/code&gt; — one vocabulary, one place it's defined, zero connectors reimplementing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The orchestrator: &lt;code&gt;SynchronizeSourceAction&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is the part that runs a sync from start to finish, and it's deliberately boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SynchronizeSourceAction&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Source&lt;/span&gt; &lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;SyncRun&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"sync:source:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$lock&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SourceSyncInProgressException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$lock&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Source&lt;/span&gt; &lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;SyncRun&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$connector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$connector&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validateConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$seenOfferIds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

        &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$connector&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$cursor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;offers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$dto&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$persisted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;persistOffer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$dto&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="nv"&gt;$seenOfferIds&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$persisted&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;offerId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;nextCursor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$page&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;hasMorePages&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nv"&gt;$cursor&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;expireStaleOffers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$seenOfferIds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// ...record a SyncRun with stats, return it&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three decisions here are doing more work than the line count suggests:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A per-source lock, not a global one.&lt;/strong&gt; Two syncs of the &lt;em&gt;same&lt;/em&gt; source can't overlap (no point double-fetching the same French Travail search), but syncing eight sources in parallel is completely fine, because they never touch each other's lock key.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;Connector&lt;/code&gt; is resolved from a registry keyed by a driver string stored on the &lt;code&gt;Source&lt;/code&gt; row&lt;/strong&gt;, not hardcoded per source. Adding source #9 means writing a class and adding one line to &lt;code&gt;config/connectors.php&lt;/code&gt; — nothing about &lt;code&gt;SynchronizeSourceAction&lt;/code&gt; changes, ever.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale-offer expiry is a side effect of "what did we see this run," not a separate job.&lt;/strong&gt; Every offer ID that came back from &lt;code&gt;persistOffer-&amp;gt;execute()&lt;/code&gt; goes into &lt;code&gt;$seenOfferIds&lt;/code&gt;. After the loop, anything this source owns that's still marked &lt;code&gt;New&lt;/code&gt; but &lt;em&gt;wasn't&lt;/em&gt; in that set gets flipped to &lt;code&gt;Expired&lt;/code&gt;. No offer is ever deleted — a posting that's gone from the source just stops being active, and if it comes back next run, &lt;code&gt;PersistOfferAction&lt;/code&gt; un-expires it automatically. This assumes a connector returns its &lt;em&gt;complete&lt;/em&gt; current listing every run, which is true for all eight sources today; a delta-only API would need a different reconciliation strategy, and that's written down as a known limitation rather than quietly assumed away.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interesting part: &lt;code&gt;PersistOfferAction&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Deduplication across eight independently-scraped sources is the part that could have turned into a mess, so it's worth showing the actual decision tree. &lt;code&gt;OfferDeduplicator::findMatch()&lt;/code&gt; checks four signals, in order of how much they can be trusted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Level 1 — same source, same external id → definitely the same offer&lt;/span&gt;
&lt;span class="nv"&gt;$offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Offer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'source_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&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="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'external_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$externalId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OfferMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strong&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Level 2 — identical canonical URL, any source → definitely the same offer&lt;/span&gt;
&lt;span class="nv"&gt;$offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Offer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'canonical_url'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$canonicalUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OfferMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strong&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Level 3 — same content fingerprint (title+company+description hash) → *probably*&lt;/span&gt;
&lt;span class="nv"&gt;$offer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Offer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'content_hash'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$contentHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OfferMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strong&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Level 4 — same title+company(+location) → *probably*&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;strong&lt;/code&gt; flag is the whole trick. Levels 1 and 2 are exact evidence — the same job board can't hand you two different external IDs for one posting, and two sources publishing the identical canonical URL are, by construction, the same listing. Those get merged: the existing row is updated in place.&lt;/p&gt;

&lt;p&gt;Levels 3 and 4 are &lt;em&gt;signals&lt;/em&gt;, not proof. A title/company/location match could easily be two different postings for the same role at the same company, published a week apart. So a non-strong match never merges automatically — it creates a new offer row and tags it &lt;code&gt;possible_duplicate_of&lt;/code&gt; for manual review. I'd rather show a false-positive "possible duplicate" once in a while than silently collapse two distinct job postings into one and lose data.&lt;/p&gt;

&lt;p&gt;From there, &lt;code&gt;PersistOfferAction&lt;/code&gt; branches into exactly three outcomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No match → create.&lt;/strong&gt; Straightforward insert, technologies attached, done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong match, same source owns it → update.&lt;/strong&gt; Title, salary, description, everything gets refreshed. If the offer had expired and reappeared in the feed, its status flips back to &lt;code&gt;New&lt;/code&gt; — but a manual decision (favorited, archived, ignored) is &lt;em&gt;never&lt;/em&gt; touched by an automated sync.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strong match, a &lt;em&gt;different&lt;/em&gt; source owns it → enrich, don't overwrite.&lt;/strong&gt; This is the one that surprised me the most when I first wrote it. If Adzuna and Free-Work both list the same posting and Free-Work saw it first, Adzuna's copy doesn't overwrite Free-Work's data — it only fills in fields Free-Work left &lt;code&gt;null&lt;/code&gt;. The owning source's data always wins; a later source can only patch gaps, never clobber.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;enrichForeignOffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Offer&lt;/span&gt; &lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;NormalizedOffer&lt;/span&gt; &lt;span class="nv"&gt;$dto&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$gaps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'company'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$dto&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;company&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'salary_min'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$dto&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;salaryMinimum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'location'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$dto&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$gaps&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$attribute&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$attribute&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$attribute&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$offer&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Where it got tested for real: adding a source that isn't a network request
&lt;/h2&gt;

&lt;p&gt;Every connector I described above does the same thing at the transport level: send an HTTP request, get a response, parse it. Then I wanted to add Jooble — a French job aggregator — as a source. The catch: their site sits behind a Cloudflare bot challenge on every page (including &lt;code&gt;robots.txt&lt;/code&gt;-adjacent ones), and their terms of service explicitly prohibit automated crawlers. Their official API exists but is out of scope for how I wanted to use it here.&lt;/p&gt;

&lt;p&gt;What Jooble &lt;em&gt;does&lt;/em&gt; do is email me job alerts, because I subscribed to one like anyone would. So instead of an HTTP connector, I wrote one that reads a mailbox over IMAP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;JoobleEmailConnector&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Connector&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;MailboxContract&lt;/span&gt; &lt;span class="nv"&gt;$mailbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;JoobleEmailParser&lt;/span&gt; &lt;span class="nv"&gt;$parser&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Source&lt;/span&gt; &lt;span class="nv"&gt;$source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;ConnectorPage&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetchUnseenMessages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'connectors.jooble_email.max_messages_per_sync'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;JoobleEmailParser&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SENDER_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$offers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$messages&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// not a Jooble email — leave it unread for another parser&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$offers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$offers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;mailbox&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;markProcessed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConnectorPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offers&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$offers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nextCursor&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hasMorePages&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still just a &lt;code&gt;Connector&lt;/code&gt;. &lt;code&gt;SynchronizeSourceAction&lt;/code&gt; doesn't know or care that &lt;code&gt;fetch()&lt;/code&gt; opened an IMAP session instead of an HTTP one. &lt;code&gt;PersistOfferAction&lt;/code&gt; doesn't know or care that the &lt;code&gt;NormalizedOffer&lt;/code&gt;s it's deduplicating came from parsing &lt;code&gt;&amp;lt;span class="tag"&amp;gt;&lt;/code&gt; badges in an HTML email instead of a JSON-LD block. The entire sync pipeline — locking, pagination, stale-offer expiry, the four-level dedup, junior/senior classification — is exactly as much code as it was before this source existed: zero new lines.&lt;/p&gt;

&lt;p&gt;The IMAP "unread flag" turned out to be a nice fit for the cursor concept too — a message only gets flagged &lt;code&gt;\Seen&lt;/code&gt; once it's been successfully parsed, so "what's new since last time" falls out of the mailbox itself instead of needing a persisted cursor. (I did get this wrong on the first pass: fetching "the first N unread messages" without filtering by sender first meant a mailbox with 30,000 unread emails buried the ~450 Jooble ones completely. The fix was filtering server-side, over IMAP, before applying the limit — a good reminder that "unread" and "relevant" are not the same predicate, and testing against realistic mailbox contents rather than a couple of fixtures would have caught it sooner.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone building this from scratch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Draw the abstraction boundary at the data, not the transport.&lt;/strong&gt; "Fetch and parse" is where sources genuinely differ; "decide if this is a new offer" is where they don't. Put the interface exactly on that seam.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nullable-by-default DTOs make "we don't know" a real, representable state&lt;/strong&gt;, instead of a temptation to guess. It costs you some &lt;code&gt;?string&lt;/code&gt; noise; it saves you from a parser inventing data three months later when nobody remembers it did.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not all duplicate signals deserve the same trust&lt;/strong&gt;, and conflating them is how you either merge two different jobs into one or spam yourself with false duplicates. Give strong evidence the power to merge and weak evidence only the power to flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real test of an abstraction is adding something that doesn't fit the mold you designed it around.&lt;/strong&gt; For me that was a mailbox instead of an HTTP client. If your interface survives that with zero changes to the orchestrator, it was drawn in the right place.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>architecture</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Structuring a Laravel Project with Repositories and Services (Without Over-Engineering)</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Mon, 06 Apr 2026 14:40:03 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/structuring-a-laravel-project-with-repositories-and-services-without-over-engineering-i2</link>
      <guid>https://dev.to/blamsa0mine/structuring-a-laravel-project-with-repositories-and-services-without-over-engineering-i2</guid>
      <description>&lt;p&gt;A while ago, I wrote about using repositories and services in Laravel.&lt;/p&gt;

&lt;p&gt;At the time, the goal was simple: &lt;strong&gt;keep controllers cleaner&lt;/strong&gt; and &lt;strong&gt;move logic to dedicated layers&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I still agree with that idea.&lt;/p&gt;

&lt;p&gt;But with more experience, I would structure that article differently today.&lt;/p&gt;

&lt;p&gt;Because in Laravel, there is always a risk when talking about architecture:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;we can improve code organization, but we can also over-engineer very quickly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Laravel already gives us a lot out of the box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Eloquent&lt;/li&gt;
&lt;li&gt;Form Requests&lt;/li&gt;
&lt;li&gt;API Resources&lt;/li&gt;
&lt;li&gt;Route model binding&lt;/li&gt;
&lt;li&gt;Events and jobs&lt;/li&gt;
&lt;li&gt;A powerful service container&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the real question is not:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Should I always use repositories and services?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“At what point do these layers actually make my code easier to maintain ?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the angle of this rewritten article.&lt;/p&gt;

&lt;p&gt;I want to show a more practical version of the repository + service approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;controllers&lt;/strong&gt; for HTTP concerns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;services&lt;/strong&gt; for business rules and orchestration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;repositories&lt;/strong&gt; for data access only when that logic becomes worth isolating&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why I would revisit the original approach
&lt;/h2&gt;

&lt;p&gt;A lot of repository pattern tutorials in Laravel follow the same structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a generic &lt;code&gt;BaseRepository&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;CRUD methods like &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;delete&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a service that mostly forwards calls to the repository&lt;/li&gt;
&lt;li&gt;a controller that becomes a little cleaner&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At first glance, that seems fine.&lt;/p&gt;

&lt;p&gt;But after building more real-world features, I think there is one important nuance:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;if a repository is only a thin wrapper around Eloquent, it often adds indirection more than value.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if the service layer only forwards method calls one by one, it is not really a business layer either.&lt;/p&gt;

&lt;p&gt;That does not mean repositories and services are bad.&lt;/p&gt;

&lt;p&gt;It means they become useful when they have a &lt;strong&gt;clear responsibility&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the version I would recommend today.&lt;/p&gt;




&lt;h2&gt;
  
  
  When this structure is actually useful
&lt;/h2&gt;

&lt;p&gt;I would not add repositories and services to every small Laravel CRUD app.&lt;/p&gt;

&lt;p&gt;For a very simple feature, using Eloquent directly in a controller can be acceptable, especially early on.&lt;/p&gt;

&lt;p&gt;This structure becomes much more useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;query logic is reused in several places&lt;/li&gt;
&lt;li&gt;business rules start growing&lt;/li&gt;
&lt;li&gt;writes involve multiple actions or side effects&lt;/li&gt;
&lt;li&gt;controllers are getting too large&lt;/li&gt;
&lt;li&gt;you want better testability around domain logic&lt;/li&gt;
&lt;li&gt;you want to separate “how data is stored” from “what the app should do”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this is not about turning every Laravel project into enterprise architecture.&lt;/p&gt;

&lt;p&gt;It is about introducing structure &lt;strong&gt;when it starts paying for itself&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The main issue with generic BaseRepository examples
&lt;/h2&gt;

&lt;p&gt;In my opinion, the biggest weakness in many examples is the generic &lt;code&gt;BaseRepository&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You often see something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;BaseRepositoryInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getAll&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then a base implementation shared by every model.&lt;/p&gt;

&lt;p&gt;The problem is that this often ends up duplicating what Eloquent already does very well.&lt;/p&gt;

&lt;p&gt;Even worse, the abstraction starts leaking quickly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;sooner or later you need custom queries&lt;/li&gt;
&lt;li&gt;then you add model-specific methods&lt;/li&gt;
&lt;li&gt;then the “generic” abstraction stops being generic anyway&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So instead of starting from a generic repository, I now prefer starting from a &lt;strong&gt;feature-oriented repository&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means the interface should reflect real application needs, not just CRUD verbs.&lt;/p&gt;




&lt;h2&gt;
  
  
  A more practical Laravel structure
&lt;/h2&gt;

&lt;p&gt;For a medium-sized Laravel app, I like something close to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── Http/
│   ├── Controllers/
│   │   └── Api/
│   │       └── PostController.php
│   ├── Requests/
│   │   ├── StorePostRequest.php
│   │   └── UpdatePostRequest.php
│   └── Resources/
│       └── PostResource.php
├── Models/
│   └── Post.php
├── Repositories/
│   ├── Contracts/
│   │   └── PostRepositoryInterface.php
│   └── EloquentPostRepository.php
├── Services/
│   └── PostService.php
└── Providers/
    └── AppServiceProvider.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps each responsibility clear without creating too many layers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: create a repository contract around the feature
&lt;/h2&gt;

&lt;p&gt;Instead of a generic base repository, let’s define a contract that reflects what the feature actually needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Repositories&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Contracts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Models&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Contracts&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Pagination&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\LengthAwarePaginator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;interface&lt;/span&gt; &lt;span class="nc"&gt;PostRepositoryInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;paginatePublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$perPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;LengthAwarePaginator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?Post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few details are intentional here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;findById()&lt;/code&gt; is clearer than a very generic &lt;code&gt;find()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;update()&lt;/code&gt; receives a &lt;code&gt;Post&lt;/code&gt; instance, not only an &lt;code&gt;id&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;delete()&lt;/code&gt; also receives the model instance, which fits Eloquent better&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is already more expressive than a generic CRUD shell.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: implement the repository with Eloquent
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Repositories&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Models&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Repositories&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Contracts&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\PostRepositoryInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Contracts&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Pagination&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\LengthAwarePaginator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EloquentPostRepository&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;PostRepositoryInterface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;paginatePublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$perPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;LengthAwarePaginator&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'is_published'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$perPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;findById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?Post&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Post&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Post&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$attributes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;refresh&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This repository stays simple, but it already has real value:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;query logic is centralized&lt;/li&gt;
&lt;li&gt;data access is kept out of controllers&lt;/li&gt;
&lt;li&gt;the contract can be mocked in service tests&lt;/li&gt;
&lt;li&gt;implementation details can evolve later without touching other layers&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3: put real business logic in the service layer
&lt;/h2&gt;

&lt;p&gt;This is the part where I changed my mind the most over time.&lt;/p&gt;

&lt;p&gt;I no longer think a service should exist just to forward calls to the repository.&lt;/p&gt;

&lt;p&gt;If a service does not make decisions, orchestrate actions, or apply business rules, it may not be worth having.&lt;/p&gt;

&lt;p&gt;Here is a more useful service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Services&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Models&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Repositories&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Contracts&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\PostRepositoryInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Contracts&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Pagination&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\LengthAwarePaginator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Support&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Facades&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="no"&gt;\DB&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Support&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Str&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;PostRepositoryInterface&lt;/span&gt; &lt;span class="nv"&gt;$posts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;listPublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$perPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;LengthAwarePaginator&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;paginatePublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$perPage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Post&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'slug'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
                &lt;span class="s1"&gt;'is_published'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'is_published'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;];&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Post&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'content'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="s1"&gt;'is_published'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'is_published'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;is_published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;];&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'slug'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'title'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;posts&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the service has a real purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preparing the payload&lt;/li&gt;
&lt;li&gt;generating the slug&lt;/li&gt;
&lt;li&gt;centralizing write rules&lt;/li&gt;
&lt;li&gt;wrapping write operations in transactions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the type of service layer I find useful in Laravel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: move validation to Form Requests
&lt;/h2&gt;

&lt;p&gt;If you want thinner controllers, moving validation out of them is one of the easiest wins.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Requests&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Foundation&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\FormRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;StorePostRequest&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;FormRequest&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'max:255'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'required'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'string'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'is_published'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'sometimes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'boolean'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for updates, I would usually create a dedicated &lt;code&gt;UpdatePostRequest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That keeps each action explicit and prevents “one giant request class” from growing over time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: use API Resources for response formatting
&lt;/h2&gt;

&lt;p&gt;When building JSON APIs, I also prefer keeping response transformation out of controllers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Resources&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Resources&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Json&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\JsonResource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;JsonResource&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;toArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'slug'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'content'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'is_published'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;is_published&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'created_at'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That becomes especially useful when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;your API grows&lt;/li&gt;
&lt;li&gt;fields need conditional exposure&lt;/li&gt;
&lt;li&gt;you want stable response formatting&lt;/li&gt;
&lt;li&gt;you want to avoid repeating transformation logic everywhere&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 6: keep the controller focused on HTTP
&lt;/h2&gt;

&lt;p&gt;At this point, the controller becomes much easier to read.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Controllers&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Controllers&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Controller&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Requests&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\StorePostRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Resources&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\PostResource&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Models&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Post&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Services&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\PostService&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\JsonResponse&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Http&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Resources&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Json&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\AnonymousResourceCollection&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kt"&gt;PostService&lt;/span&gt; &lt;span class="nv"&gt;$postService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;AnonymousResourceCollection&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$perPage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'per_page'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;collection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;postService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;listPublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$perPage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StorePostRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;PostResource&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;postService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;PostResource&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PostResource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Post&lt;/span&gt; &lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;JsonResponse&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;postService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$post&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
            &lt;span class="s1"&gt;'message'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'Post deleted successfully.'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of controller I like seeing in Laravel projects.&lt;/p&gt;

&lt;p&gt;It reads almost like a checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validate the request&lt;/li&gt;
&lt;li&gt;call the service&lt;/li&gt;
&lt;li&gt;return the response&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No heavy query logic.&lt;br&gt;
No business rules scattered in actions.&lt;br&gt;
No response formatting noise.&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 7: bind the repository in the service container
&lt;/h2&gt;

&lt;p&gt;Now we bind the interface to the implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;declare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strict_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Providers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Repositories&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Contracts&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\PostRepositoryInterface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Repositories&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\EloquentPostRepository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\Support&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="nc"&gt;\ServiceProvider&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppServiceProvider&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;ServiceProvider&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PostRepositoryInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;EloquentPostRepository&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is one of the reasons the pattern becomes interesting:&lt;/p&gt;

&lt;p&gt;you code against a contract, while Laravel resolves the concrete implementation for you.&lt;/p&gt;

&lt;p&gt;That makes later changes easier, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;replacing the implementation&lt;/li&gt;
&lt;li&gt;decorating it with caching&lt;/li&gt;
&lt;li&gt;mocking it in tests&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What is improved compared to the older version
&lt;/h2&gt;

&lt;p&gt;Here is what I would consider a real improvement over the more classic repository/service article style.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The repository is not generic for the sake of being generic
&lt;/h3&gt;

&lt;p&gt;The repository is tied to a feature and exposes meaningful methods.&lt;/p&gt;

&lt;p&gt;That makes the abstraction easier to justify.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The service contains actual business logic
&lt;/h3&gt;

&lt;p&gt;It is not just a pass-through layer.&lt;/p&gt;

&lt;p&gt;It prepares payloads, manages transactions, and centralizes behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The controller is smaller for the right reasons
&lt;/h3&gt;

&lt;p&gt;Not because logic disappeared into random files, but because each concern moved to the right place.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The code becomes easier to test
&lt;/h3&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;feature test the controller&lt;/li&gt;
&lt;li&gt;unit test the service with a mocked repository&lt;/li&gt;
&lt;li&gt;integration test the repository against the database&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That testing split feels much more natural.&lt;/p&gt;




&lt;h2&gt;
  
  
  When I would not use repositories and services
&lt;/h2&gt;

&lt;p&gt;I think this part is just as important.&lt;/p&gt;

&lt;p&gt;I would probably avoid this structure when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the feature is tiny&lt;/li&gt;
&lt;li&gt;it is a quick prototype&lt;/li&gt;
&lt;li&gt;Eloquent already expresses the logic clearly enough&lt;/li&gt;
&lt;li&gt;there is no meaningful business logic yet&lt;/li&gt;
&lt;li&gt;abstraction would only create extra files and extra indirection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laravel is already expressive.&lt;/p&gt;

&lt;p&gt;Sometimes the cleanest code is simply a small controller using Eloquent responsibly.&lt;/p&gt;

&lt;p&gt;That is why I now see repositories and services as &lt;strong&gt;tools&lt;/strong&gt;, not &lt;strong&gt;defaults&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I still think repositories and services can improve a Laravel codebase a lot.&lt;/p&gt;

&lt;p&gt;But today, I would frame it differently:&lt;/p&gt;

&lt;p&gt;the goal is not to “add architecture”.&lt;/p&gt;

&lt;p&gt;The goal is to &lt;strong&gt;make responsibilities obvious&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A good balance often looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;controllers&lt;/strong&gt; handle HTTP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Form Requests&lt;/strong&gt; handle validation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;services&lt;/strong&gt; handle business rules and orchestration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;repositories&lt;/strong&gt; handle data access when it is worth isolating&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resources&lt;/strong&gt; shape API responses&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is usually enough structure to keep a Laravel application clean without making it unnecessarily complex.&lt;/p&gt;

&lt;p&gt;And honestly, that is the sweet spot I try to aim for now.&lt;/p&gt;

</description>
      <category>php</category>
      <category>laravel</category>
      <category>cleancoding</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I Built a Support Ticket MCP Server in Laravel</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Tue, 31 Mar 2026 18:57:32 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/how-i-built-a-support-ticket-mcp-server-in-laravel-1pdk</link>
      <guid>https://dev.to/blamsa0mine/how-i-built-a-support-ticket-mcp-server-in-laravel-1pdk</guid>
      <description>&lt;p&gt;Model Context Protocol is much easier to understand when it is attached to a real workflow.&lt;/p&gt;

&lt;p&gt;In my Laravel project, I wanted a support-focused MCP server that could do three concrete things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create a support ticket&lt;/li&gt;
&lt;li&gt;read a ticket from a resource URI&lt;/li&gt;
&lt;li&gt;generate a polished summary that is ready to send to a customer or teammate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of building a generic demo, I kept the scope narrow and useful. The result is a local &lt;code&gt;support&lt;/code&gt; MCP server powered by &lt;code&gt;laravel/mcp&lt;/code&gt;, with one tool, one resource, and one prompt working together as a small support system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is a good MCP use case
&lt;/h2&gt;

&lt;p&gt;A lot of MCP examples stop at "here is a tool that returns some JSON." That is fine for learning the API, but it does not show why the protocol is interesting.&lt;/p&gt;

&lt;p&gt;Support workflows are a better fit because they combine three different kinds of interaction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an action that changes state,&lt;/li&gt;
&lt;li&gt;a resource that can be fetched again later,&lt;/li&gt;
&lt;li&gt;a prompt that turns raw operational data into something a human can actually use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That maps cleanly to MCP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tool&lt;/strong&gt;: create the ticket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource&lt;/strong&gt;: fetch the ticket by identifier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt&lt;/strong&gt;: transform the ticket into a professional summary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation is what I like most about the approach. The AI client does not need custom knowledge about my Laravel app. It only needs to know which MCP capabilities are available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering the server in Laravel
&lt;/h2&gt;

&lt;p&gt;The first step in my project was registering a local MCP server in &lt;code&gt;routes/ai.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Mcp\Servers\SupportServer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Laravel\Mcp\Facades\Mcp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Mcp&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;local&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'support'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;SupportServer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives the app a dedicated MCP entry point for support operations. I also have a separate CRM server in the same project, which keeps responsibilities isolated instead of pushing every AI capability into one giant surface area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Defining the support server
&lt;/h2&gt;

&lt;p&gt;The server itself is intentionally small. Its job is to compose capabilities, not to hold business logic. In &lt;code&gt;app/Mcp/Servers/SupportServer.php&lt;/code&gt;, I register the three pieces that make the workflow useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SupportServer&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Server&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;CreateTicketTool&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$resources&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;TicketResource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$prompts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;SummarizeTicketPrompt&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That structure is one of the reasons Laravel feels comfortable here. The framework already encourages small classes with clear responsibilities, and &lt;code&gt;laravel/mcp&lt;/code&gt; fits that style well.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ticket creation tool
&lt;/h2&gt;

&lt;p&gt;The first capability I needed was a way for an AI client to create a support ticket safely.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;app/Mcp/Tools/CreateTicketTool.php&lt;/code&gt;, the tool validates three inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;title&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;description&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;priority&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It then creates the ticket and returns a structured response with the fields an agent actually needs next:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;structured&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'status'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an important detail. I do not want the tool to return a blob of unstructured text when the next step may depend on the new ticket ID. MCP tools are much more useful when they return predictable data that another step can consume immediately.&lt;/p&gt;

&lt;p&gt;I also defined the schema explicitly. That gives the client a machine-readable contract instead of forcing it to guess the arguments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;JsonSchema&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;'title'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'description'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="s1"&gt;'priority'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$schema&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;enum&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;'low'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'medium'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'high'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'medium'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For support workflows, this matters a lot. Validation rules are not just a backend concern anymore. They become part of the interface between your Laravel app and the AI client.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ticket resource
&lt;/h2&gt;

&lt;p&gt;Creating a ticket is only half of the story. Once the ticket exists, an agent should be able to retrieve it again without calling a mutation tool.&lt;/p&gt;

&lt;p&gt;That is where the resource comes in. In &lt;code&gt;app/Mcp/Resources/TicketResource.php&lt;/code&gt;, the resource receives a ticket identifier, loads the record, and returns a readable text representation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;"Ticket #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"Titre: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"Statut: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"Priorité: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;
    &lt;span class="s2"&gt;"Description:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$ticket&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I like this pattern because it makes the ticket easy to reuse in multiple contexts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an assistant can read it before answering a user&lt;/li&gt;
&lt;li&gt;another prompt can summarize it&lt;/li&gt;
&lt;li&gt;a human can inspect the output without decoding JSON&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a small design choice, but it makes the system more composable. A tool changes state. A resource exposes state. Keeping those responsibilities separate leads to cleaner MCP surfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  The prompt that turns raw data into a ready-to-send summary
&lt;/h2&gt;

&lt;p&gt;The third piece is the one that makes the demo feel like a real support workflow rather than a CRUD exercise.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;app/Mcp/Prompts/SummarizeTicketPrompt.php&lt;/code&gt;, I created a prompt that takes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;tone&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the raw &lt;code&gt;ticket_text&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt then instructs the model to return a structured support summary covering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the problem&lt;/li&gt;
&lt;li&gt;the impact&lt;/li&gt;
&lt;li&gt;the suggested priority&lt;/li&gt;
&lt;li&gt;the next action.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the core idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"You are a support assistant. Summarize the ticket below in a &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$tone&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; tone. "&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;
        &lt;span class="s2"&gt;"Return: problem, impact, suggested priority, next action."&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;asAssistant&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ticket_text&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the part many teams skip. They expose data, but they do not shape the final output. In support, the final wording matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;internal teams may want concise operational summaries&lt;/li&gt;
&lt;li&gt;customer-facing teams may want a more professional tone&lt;/li&gt;
&lt;li&gt;some situations need a friendlier message without losing the technical facts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Packaging that as an MCP prompt means the formatting logic is reusable instead of being reinvented in every client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I like this architecture
&lt;/h2&gt;

&lt;p&gt;What I ended up with is not a huge system. It is a very small one. That is exactly why it works well.&lt;/p&gt;

&lt;p&gt;Each MCP primitive has a clear role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;tool&lt;/strong&gt; creates the record,&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;resource&lt;/strong&gt; exposes the record,&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;prompt&lt;/strong&gt; transforms the record into communication.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation makes the project easier to extend. If I want to go further, I already know the next logical steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add a tool to update ticket status,&lt;/li&gt;
&lt;li&gt;expose a resource for recent tickets,&lt;/li&gt;
&lt;li&gt;create prompts for escalation notes or customer replies,&lt;/li&gt;
&lt;li&gt;connect the same support server to external AI clients.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laravel gives me the application structure, validation, and persistence patterns I already trust. MCP adds a clean interface layer on top of that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The main lesson from this project is that MCP becomes much more compelling when you model a real workflow instead of isolated demo commands.&lt;/p&gt;

&lt;p&gt;In my Laravel app, the support server is useful because it does three connected things well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;it creates a ticket with validated input,&lt;/li&gt;
&lt;li&gt;it lets the client read that ticket back as a resource,&lt;/li&gt;
&lt;li&gt;it provides a prompt that converts raw ticket data into a professional summary.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is enough to move from "AI can call my backend" to "AI can participate in a support flow with clear boundaries."&lt;/p&gt;

&lt;p&gt;If you are experimenting with &lt;code&gt;laravel/mcp&lt;/code&gt;, I would strongly recommend starting with a narrow domain like support, sales handoff, onboarding, or incident triage. Pick one workflow, then model it with one tool, one resource, and one prompt. You will learn much more than you would from a generic demo.&lt;/p&gt;

&lt;p&gt;If I keep iterating on this project, the next step will be turning this support server into a fuller ticket operations layer with richer resources and multi-step support actions. But even in its current form, it already shows why Laravel and MCP fit together so naturally.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>ai</category>
      <category>mcp</category>
    </item>
    <item>
      <title>PHP 8.5: The Changes That Really Matter in Everyday Development</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Wed, 25 Mar 2026 19:09:06 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/php-85-the-changes-that-really-matter-in-everyday-development-1pdl</link>
      <guid>https://dev.to/blamsa0mine/php-85-the-changes-that-really-matter-in-everyday-development-1pdl</guid>
      <description>&lt;p&gt;PHP 8.5 brings a solid mix of language improvements, practical extension updates, and deprecations worth cleaning up before PHP 9.0.&lt;/p&gt;

&lt;p&gt;It is not a flashy release built around one massive headline feature. Instead, it improves day-to-day developer experience in a lot of useful places: cleaner data transformations, better URL handling, nicer debugging, a few welcome standard library additions, and a clearer path away from old legacy patterns.&lt;/p&gt;

&lt;p&gt;In this article, I want to focus on what actually matters when writing and maintaining PHP code: what you can start using right away, what deserves attention during migration, and which changes are most useful in real-world projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipe operator makes transformations easier to read
&lt;/h2&gt;

&lt;p&gt;One of the most visible additions in PHP 8.5 is the pipe operator, &lt;code&gt;|&amp;gt;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Its purpose is simple: pass the result of one expression into the next one from left to right. That makes chained transformations much easier to read than deeply nested function calls.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="nv"&gt;$title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'  PHP 8.5 Released  '&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$slug&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$title&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&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;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&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;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$slug&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// php-8.5-released&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially nice for string processing, array transformations, or small data-cleaning pipelines.&lt;/p&gt;

&lt;p&gt;The biggest benefit is readability. You read the code in the same order it runs.&lt;/p&gt;

&lt;p&gt;One thing to keep in mind: functions that need multiple arguments, like &lt;code&gt;str_replace()&lt;/code&gt;, will often require a small closure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The new URI extension is one of the most useful additions
&lt;/h2&gt;

&lt;p&gt;PHP 8.5 also introduces a built-in URI extension.&lt;/p&gt;

&lt;p&gt;This is a big deal because URL handling in many projects still relies on a mix of &lt;code&gt;parse_url()&lt;/code&gt;, string concatenation, and custom helpers. That usually works until it does not.&lt;/p&gt;

&lt;p&gt;The new API gives PHP a cleaner and more structured way to work with URIs and URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Uri\Rfc3986\Uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://example.com/blog/php-8-5'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/blog/php-8-5/comments'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withQuery&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'page=2&amp;amp;sort=recent'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$updated&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="c1"&gt;// https://example.com/blog/php-8-5/comments?page=2&amp;amp;sort=recent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes a lot of sense in apps dealing with pagination, canonical URLs, crawlers, API clients, or SEO tooling.&lt;/p&gt;

&lt;p&gt;It is one of those additions that will probably save more bugs than it gets credit for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small new functions that remove a lot of noise
&lt;/h2&gt;

&lt;p&gt;PHP 8.5 also adds several small functions and APIs that reduce boilerplate.&lt;/p&gt;

&lt;p&gt;Among the most useful are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;array_first()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;array_last()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_error_handler()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get_exception_handler()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Locale::isRightToLeft()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;locale_is_right_to_left()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IntlListFormatter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FILTER_THROW_ON_FAILURE&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a simple example with &lt;code&gt;array_first()&lt;/code&gt; and &lt;code&gt;array_last()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'PHP'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Laravel'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Vue'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;array_first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;// PHP&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;array_last&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// Vue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is one with &lt;code&gt;FILTER_THROW_ON_FAILURE&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;filter_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'vincent@@example.com'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;FILTER_VALIDATE_EMAIL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;FILTER_THROW_ON_FAILURE&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'Invalid email'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these changes are huge on their own.&lt;/p&gt;

&lt;p&gt;But together, they make code a little more direct and a little less repetitive, which is exactly the kind of improvement that ages well.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;#[\NoDiscard]&lt;/code&gt; helps prevent silent mistakes
&lt;/h2&gt;

&lt;p&gt;Another interesting addition is the &lt;code&gt;#[\NoDiscard]&lt;/code&gt; attribute.&lt;/p&gt;

&lt;p&gt;It allows you to mark a function whose return value should not be ignored.&lt;/p&gt;

&lt;p&gt;That is especially useful for transformation functions where forgetting to use the returned value is usually a bug.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="na"&gt;#[\NoDiscard]&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;strtolower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$email&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normalizeEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'  Vincent.Capek@Example.COM  '&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP 8.5 also introduces the &lt;code&gt;(void)&lt;/code&gt; cast so you can explicitly ignore a return value when that is intentional.&lt;/p&gt;

&lt;p&gt;This makes intent clearer and helps distinguish real mistakes from deliberate choices.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fatal errors are more useful now
&lt;/h2&gt;

&lt;p&gt;PHP 8.5 improves debugging by adding stack traces to fatal errors.&lt;/p&gt;

&lt;p&gt;This sounds small, but in practice it is a very welcome quality-of-life improvement.&lt;/p&gt;

&lt;p&gt;When something crashes badly, getting more context immediately can save time, especially in larger apps or older codebases where tracking the source of a fatal error can be annoying.&lt;/p&gt;

&lt;p&gt;It is one of those improvements you do not think about until the moment you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical extension improvements you will actually notice
&lt;/h2&gt;

&lt;p&gt;PHP 8.5 also includes several useful updates across built-in extensions.&lt;/p&gt;

&lt;p&gt;A few stand out more than others in day-to-day work.&lt;/p&gt;

&lt;h3&gt;
  
  
  DOM gets &lt;code&gt;outerHTML&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Dom\Element::$outerHTML&lt;/code&gt; is finally available.&lt;/p&gt;

&lt;p&gt;That is great for scraping, HTML cleanup, testing, or any tool that manipulates markup.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="nv"&gt;$doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dom\HTMLDocument&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;createFromString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s1"&gt;'&amp;lt;article class="post"&amp;gt;&amp;lt;h1&amp;gt;Hello&amp;lt;/h1&amp;gt;&amp;lt;/article&amp;gt;'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$doc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'article.post'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$article&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;outerHTML&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;getimagesize()&lt;/code&gt; improves support for modern formats
&lt;/h3&gt;

&lt;p&gt;PHP 8.5 improves &lt;code&gt;getimagesize()&lt;/code&gt; support for HEIF, HEIC, and SVG.&lt;/p&gt;

&lt;p&gt;That is genuinely useful for uploads, media libraries, CMS features, and portfolio-like projects where modern image formats show up more often.&lt;/p&gt;

&lt;h3&gt;
  
  
  Partitioned cookies are now supported
&lt;/h3&gt;

&lt;p&gt;Cookies and sessions now support the &lt;code&gt;partitioned&lt;/code&gt; flag.&lt;/p&gt;

&lt;p&gt;This matters more in modern browser environments where third-party cookie behavior has become stricter.&lt;/p&gt;

&lt;h3&gt;
  
  
  cURL exposes more diagnostic information
&lt;/h3&gt;

&lt;p&gt;PHP 8.5 adds more details to &lt;code&gt;curl_getinfo()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That helps when debugging outgoing HTTP requests, proxies, authentication behavior, or connection timing.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;mail()&lt;/code&gt; reports failures more honestly
&lt;/h3&gt;

&lt;p&gt;If you still use &lt;code&gt;mail()&lt;/code&gt; in some environment, PHP 8.5 improves error reporting around sendmail failures.&lt;/p&gt;

&lt;p&gt;That does not modernize &lt;code&gt;mail()&lt;/code&gt;, but it does make it less opaque.&lt;/p&gt;

&lt;h2&gt;
  
  
  The deprecations are not scary, but they are worth cleaning up
&lt;/h2&gt;

&lt;p&gt;PHP 8.5 also deprecates a number of old patterns.&lt;/p&gt;

&lt;p&gt;Most of them are not difficult to fix, but they are exactly the sort of things that can sit around in a codebase for years if ignored.&lt;/p&gt;

&lt;p&gt;Some of the most important ones are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;case;&lt;/code&gt; instead of &lt;code&gt;case:&lt;/code&gt; in &lt;code&gt;switch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;non-canonical casts like &lt;code&gt;(boolean)&lt;/code&gt;, &lt;code&gt;(integer)&lt;/code&gt;, &lt;code&gt;(double)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;backticks as an alias for &lt;code&gt;shell_exec()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;__sleep()&lt;/code&gt; and &lt;code&gt;__wakeup()&lt;/code&gt; in favor of &lt;code&gt;__serialize()&lt;/code&gt; and &lt;code&gt;__unserialize()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;using &lt;code&gt;null&lt;/code&gt; as an array offset or in &lt;code&gt;array_key_exists()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;incrementing non-numeric strings with &lt;code&gt;++&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;legacy functions like &lt;code&gt;curl_close()&lt;/code&gt;, &lt;code&gt;imagedestroy()&lt;/code&gt;, &lt;code&gt;finfo_close()&lt;/code&gt;, &lt;code&gt;xml_parser_free()&lt;/code&gt;, &lt;code&gt;mysqli_execute()&lt;/code&gt;, and &lt;code&gt;socket_set_timeout()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is a simple before-and-after example:&lt;/p&gt;

&lt;p&gt;Before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sb"&gt;`ls -la`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nv"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;shell_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ls -la'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'draft'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are not dramatic changes, but they are good signals.&lt;/p&gt;

&lt;p&gt;They push PHP code toward more consistency and away from old habits that are either confusing, unnecessary, or easy to misuse.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would prioritize first in a real migration
&lt;/h2&gt;

&lt;p&gt;If I were upgrading a real project to PHP 8.5, I would focus on three things first.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Try the features that can simplify real code
&lt;/h3&gt;

&lt;p&gt;The pipe operator and the new URI extension are the two most interesting practical additions to experiment with right away.&lt;/p&gt;

&lt;p&gt;They can make everyday code cleaner without needing a big architectural change.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Clean up easy deprecations early
&lt;/h3&gt;

&lt;p&gt;Things like &lt;code&gt;(boolean)&lt;/code&gt;, backticks, &lt;code&gt;case;&lt;/code&gt;, or legacy close/free functions are usually simple to fix and easy to detect with a global search.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Check extension-related behavior where it matters
&lt;/h3&gt;

&lt;p&gt;If your app handles HTML, images, outgoing HTTP requests, or more advanced cookie behavior, PHP 8.5 has changes worth testing directly.&lt;/p&gt;

&lt;p&gt;That is where the most practical wins are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;PHP 8.5 is not the kind of release that tries to reinvent the language.&lt;/p&gt;

&lt;p&gt;Instead, it improves the everyday experience of writing PHP.&lt;/p&gt;

&lt;p&gt;The pipe operator improves readability. The URI extension fills a long-standing gap. The new functions remove little bits of friction. Debugging gets better. Extension updates feel grounded in real use cases. And the deprecations help nudge codebases toward cleaner patterns.&lt;/p&gt;

&lt;p&gt;That makes PHP 8.5 a very solid release.&lt;/p&gt;

&lt;p&gt;Not because it changes everything, but because it improves the places where developers spend time every day.&lt;/p&gt;

</description>
      <category>php</category>
      <category>webdev</category>
      <category>programming</category>
      <category>backend</category>
    </item>
    <item>
      <title>Learning Buffalo by Building a Small Helpdesk App in Go</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Tue, 24 Mar 2026 18:27:08 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/learning-buffalo-by-building-a-small-helpdesk-app-in-go-12do</link>
      <guid>https://dev.to/blamsa0mine/learning-buffalo-by-building-a-small-helpdesk-app-in-go-12do</guid>
      <description>&lt;p&gt;When people talk about Go web development, the conversation often jumps straight to minimal stacks: &lt;code&gt;net/http&lt;/code&gt;, a router, a query builder, a templating choice, and then a lot of glue code.&lt;/p&gt;

&lt;p&gt;That approach is great when you want total control.&lt;/p&gt;

&lt;p&gt;But sometimes, you want a more complete web framework experience — something that gives you a structured project layout, routing, templates, models, migrations, forms, and a development workflow that feels closer to Laravel or Rails.&lt;/p&gt;

&lt;p&gt;That is exactly why I wanted to explore &lt;strong&gt;Buffalo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To make it concrete, I built a small example project called &lt;strong&gt;HelpDesk Lite&lt;/strong&gt;, a simple support app with users, tickets, and comments:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub repo:&lt;/strong&gt; &lt;code&gt;https://github.com/VincentCapek/helpdesk_lite&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In this article, I want to show how Buffalo works in practice through this project, what the developer experience feels like, and what I learned along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Buffalo ?
&lt;/h2&gt;

&lt;p&gt;Buffalo is interesting because it is not just "a router plus a few helpers".&lt;/p&gt;

&lt;p&gt;It gives you a full web application structure for Go:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;actions&lt;/strong&gt; for request handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;models&lt;/strong&gt; for database entities&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pop&lt;/strong&gt; for ORM-style database work and migrations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plush&lt;/strong&gt; for server-side templates&lt;/li&gt;
&lt;li&gt;a CLI that helps you run, build, and manage the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are coming from frameworks like Laravel, Rails, or even Symfony, the mental model feels familiar — but you still write Go.&lt;/p&gt;

&lt;p&gt;That mix is what makes Buffalo appealing.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Example: HelpDesk Lite
&lt;/h2&gt;

&lt;p&gt;To learn Buffalo properly, I did not want a “hello world” project. I wanted a small app with enough moving parts to exercise the framework.&lt;/p&gt;

&lt;p&gt;So I used this structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;users&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;tickets&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;comments&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a user can create a support ticket&lt;/li&gt;
&lt;li&gt;tickets have a title, description, status, priority, and category&lt;/li&gt;
&lt;li&gt;users can add comments to a ticket&lt;/li&gt;
&lt;li&gt;there is also an admin entry point in the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of project is perfect for learning because it touches most of the things you expect in a real web app:&lt;br&gt;
routing, CRUD, validation, relational data, templates, and tests.&lt;/p&gt;


&lt;h2&gt;
  
  
  What a Buffalo Project Feels Like
&lt;/h2&gt;

&lt;p&gt;One of the first things I liked about Buffalo is that the project layout pushes you toward a clean separation of concerns.&lt;/p&gt;

&lt;p&gt;In this app, the important folders are easy to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;actions/&lt;/code&gt; → request handlers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;models/&lt;/code&gt; → structs and validation&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrations/&lt;/code&gt; → schema changes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;templates/&lt;/code&gt; → Plush views&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;config/&lt;/code&gt; → application setup&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cmd/app/&lt;/code&gt; → application entry point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is a big part of the Buffalo experience:&lt;br&gt;
instead of deciding everything from scratch, you start with conventions.&lt;/p&gt;

&lt;p&gt;For small and medium-sized applications, that is a real productivity boost.&lt;/p&gt;


&lt;h2&gt;
  
  
  Routing: Very Easy to Read
&lt;/h2&gt;

&lt;p&gt;In Buffalo, routes are straightforward.&lt;/p&gt;

&lt;p&gt;For a helpdesk app, the route tree is very readable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HomeHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tickets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TicketsIndex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tickets/new"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TicketsNew&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tickets"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TicketsCreate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tickets/{ticket_id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TicketsShow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tickets/{ticket_id}/edit"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TicketsEdit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PUT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tickets/{ticket_id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TicketsUpdate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DELETE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tickets/{ticket_id}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TicketsDestroy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/tickets/{ticket_id}/comments"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CommentsCreate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AdminDashboard&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are used to MVC frameworks, this feels natural immediately.&lt;/p&gt;

&lt;p&gt;It also makes the request flow easy to follow:&lt;br&gt;
URL → action → model/database → template.&lt;/p&gt;


&lt;h2&gt;
  
  
  Actions: The Core of the Request Flow
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;actions/&lt;/code&gt; folder is where the app logic starts to feel real.&lt;/p&gt;

&lt;p&gt;A very typical Buffalo action looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;get the database transaction from the context&lt;/li&gt;
&lt;li&gt;bind request data into a struct&lt;/li&gt;
&lt;li&gt;validate the data&lt;/li&gt;
&lt;li&gt;save it with Pop&lt;/li&gt;
&lt;li&gt;either render a template again on failure or redirect on success&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That pattern appears again and again in a Buffalo app.&lt;/p&gt;

&lt;p&gt;For example, creating a ticket or comment follows the same idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;c.Bind(...)&lt;/code&gt; to map form data into a struct&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Validate(...)&lt;/code&gt; or &lt;code&gt;ValidateAndCreate(...)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tx.Create(...)&lt;/code&gt; to persist&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;c.Render(...)&lt;/code&gt; for validation errors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;c.Redirect(...)&lt;/code&gt; after success&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I like this because it keeps actions explicit.&lt;br&gt;
Nothing feels too magical.&lt;br&gt;
You can read the code top to bottom and understand the full lifecycle of the request.&lt;/p&gt;


&lt;h2&gt;
  
  
  Pop: Database Work Without Too Much Boilerplate
&lt;/h2&gt;

&lt;p&gt;Buffalo uses &lt;strong&gt;Pop&lt;/strong&gt; for database access and migrations.&lt;/p&gt;

&lt;p&gt;For a project like HelpDesk Lite, that feels like a nice middle ground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simpler than hand-writing everything with raw SQL&lt;/li&gt;
&lt;li&gt;less heavy than some ORMs that hide too much&lt;/li&gt;
&lt;li&gt;integrated well into the Buffalo workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, you work with your models as Go structs, and Pop helps you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;query data&lt;/li&gt;
&lt;li&gt;create/update/delete rows&lt;/li&gt;
&lt;li&gt;validate models&lt;/li&gt;
&lt;li&gt;manage migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That was especially useful for ticket and comment creation because the code stays readable.&lt;/p&gt;

&lt;p&gt;A request can bind data into a struct, validate it, save it, then redirect — all in a very compact way.&lt;/p&gt;


&lt;h2&gt;
  
  
  Migrations: A Good Workflow Once You Understand the Rules
&lt;/h2&gt;

&lt;p&gt;Buffalo migrations are powered by Pop, and you can write them with &lt;strong&gt;Fizz&lt;/strong&gt; or raw SQL.&lt;/p&gt;

&lt;p&gt;For this project, the migrations created tables like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;users&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tickets&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;comments&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing that is important to understand early:&lt;br&gt;
&lt;strong&gt;only proper migration files are executed by Pop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So files like these are valid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;20260318102228_create_tickets.up.fizz&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;20260318102228_create_tickets.down.fizz&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But a standalone file like &lt;code&gt;schema.sql&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; treated as a migration by &lt;code&gt;soda migrate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That was a useful lesson.&lt;/p&gt;

&lt;p&gt;Another practical point:&lt;br&gt;
once a migration is marked as applied, changing its content later does not magically re-run it.&lt;br&gt;
If you want to evolve the schema, create a &lt;strong&gt;new migration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is standard migration discipline, but Buffalo/Pop makes you feel it very quickly.&lt;/p&gt;


&lt;h2&gt;
  
  
  Plush Templates: Familiar, but You Need to Respect the Context
&lt;/h2&gt;

&lt;p&gt;Buffalo uses &lt;strong&gt;Plush&lt;/strong&gt; for templates.&lt;/p&gt;

&lt;p&gt;If you already know server-side template engines, Plush is easy to pick up:&lt;br&gt;
you print values, loop through collections, and render HTML with embedded logic.&lt;/p&gt;

&lt;p&gt;What I liked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;templates stay readable&lt;/li&gt;
&lt;li&gt;forms are easy to build&lt;/li&gt;
&lt;li&gt;rendering is tightly integrated with actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I learned the hard way:&lt;br&gt;
&lt;strong&gt;your template only sees what you explicitly put into the context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If a template uses &lt;code&gt;ticket.Title&lt;/code&gt;, the action must provide &lt;code&gt;ticket&lt;/code&gt; with &lt;code&gt;c.Set("ticket", ticket)&lt;/code&gt; before rendering.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but it is one of those details that teaches you how Buffalo thinks:&lt;br&gt;
templates are not magically connected to your action state.&lt;br&gt;
You pass the data they need, explicitly.&lt;/p&gt;

&lt;p&gt;That is actually a good thing.&lt;br&gt;
It keeps the rendering layer predictable.&lt;/p&gt;


&lt;h2&gt;
  
  
  Validation and Error Rendering
&lt;/h2&gt;

&lt;p&gt;One part I found especially educational was validation handling.&lt;/p&gt;

&lt;p&gt;The pattern is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validate the model&lt;/li&gt;
&lt;li&gt;if there are errors, put them into the context&lt;/li&gt;
&lt;li&gt;re-render the form with a &lt;code&gt;422&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;if validation passes, save and redirect&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes form handling very clear.&lt;/p&gt;

&lt;p&gt;It also reinforces a classic web application pattern:&lt;br&gt;
&lt;strong&gt;render on failure, redirect on success&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That kind of flow is where Buffalo feels very comfortable.&lt;/p&gt;


&lt;h2&gt;
  
  
  Running the App
&lt;/h2&gt;

&lt;p&gt;The basic local workflow is straightforward.&lt;/p&gt;

&lt;p&gt;Run migrations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;soda migrate &lt;span class="nt"&gt;-e&lt;/span&gt; development &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then start the app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;buffalo dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Buffalo handles the development loop.&lt;/p&gt;

&lt;p&gt;One useful detail:&lt;br&gt;
if you use SQLite, make sure your Buffalo binary actually includes SQLite support.&lt;br&gt;
I ran into that while testing the project.&lt;br&gt;
&lt;code&gt;buffalo dev&lt;/code&gt; and &lt;code&gt;soda&lt;/code&gt; are not always built the same way, so environment setup matters.&lt;/p&gt;

&lt;p&gt;That was a good reminder that frameworks improve workflow, but understanding the tooling still matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Like About Buffalo
&lt;/h2&gt;

&lt;p&gt;After building this helpdesk example, here is what stood out to me.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. It gives Go a more “full-stack framework” feel
&lt;/h3&gt;

&lt;p&gt;A lot of Go web stacks are intentionally minimal.&lt;br&gt;
Buffalo is more opinionated, and that is exactly its value.&lt;/p&gt;

&lt;p&gt;You get a real application structure instead of assembling everything manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The request flow is easy to understand
&lt;/h3&gt;

&lt;p&gt;Routes, actions, models, templates, redirects:&lt;br&gt;
everything follows a clear pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Pop and migrations are practical
&lt;/h3&gt;

&lt;p&gt;For CRUD-style applications, Pop feels productive without becoming too abstract.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The framework teaches discipline
&lt;/h3&gt;

&lt;p&gt;Buffalo makes some mistakes very visible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;missing context variables in templates&lt;/li&gt;
&lt;li&gt;incorrect migration assumptions&lt;/li&gt;
&lt;li&gt;validation/rendering mistakes&lt;/li&gt;
&lt;li&gt;SQLite setup issues in the CLI tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That might sound annoying at first, but it helps you understand the framework faster.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>go</category>
      <category>programming</category>
      <category>backend</category>
    </item>
    <item>
      <title>Building a Simple Notes REST API with Go and Fiber</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Mon, 16 Mar 2026 13:55:23 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/building-a-simple-notes-rest-api-with-go-and-fiber-ghb</link>
      <guid>https://dev.to/blamsa0mine/building-a-simple-notes-rest-api-with-go-and-fiber-ghb</guid>
      <description>&lt;p&gt;When learning a new web framework, I like to avoid huge starter kits and focus on something small, useful, and easy to reason about.&lt;/p&gt;

&lt;p&gt;That is exactly why I built a &lt;strong&gt;notes REST API&lt;/strong&gt; with &lt;strong&gt;Go&lt;/strong&gt; and &lt;strong&gt;Fiber&lt;/strong&gt;. The goal was not to create a production-ready SaaS, but to understand the pieces that matter when starting with Fiber: routing, handlers, middleware, error handling, and a clean project structure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Fiber ?
&lt;/h2&gt;

&lt;p&gt;Fiber is attractive because it gives you a very approachable developer experience while still feeling fast and lightweight. It is inspired by Express, but built for Go, and it keeps the routing and middleware story simple enough that you can focus on your API design instead of fighting the framework.&lt;/p&gt;

&lt;p&gt;For this project, I wanted something that would let me practice the basics of a REST API without adding unnecessary complexity. So instead of starting with a database, authentication, migrations, and Docker, I began with an &lt;strong&gt;in-memory notes store&lt;/strong&gt; and a simple layered structure. That let me focus on the request flow first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;

&lt;p&gt;One thing I did not want was a single &lt;code&gt;main.go&lt;/code&gt; file with everything inside it. Even for a small API, separating concerns makes the code easier to read and evolve.&lt;/p&gt;

&lt;p&gt;This is the structure I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fiber-notes/
├── main.go
├── handlers/
├── middleware/
├── models/
├── routes/
└── storage/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;main.go&lt;/code&gt; wires the application together, the handlers contain the HTTP logic, routes register endpoints, models define the data shape, storage handles persistence, and middleware contains reusable request logic like logging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bootstrapping the Fiber app
&lt;/h2&gt;

&lt;p&gt;The application setup is intentionally small. I create the Fiber app, initialize the memory store, inject it into the handler, register middleware, then mount the notes routes under &lt;code&gt;/api&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMemoryStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;noteHandler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewNoteHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;recover&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;middleware&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SimpleLogger&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Hello, World!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;"ok"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/api"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RegisterNoteRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noteHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":3000"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Defining the model
&lt;/h2&gt;

&lt;p&gt;The notes API only needs one model, and that simplicity is part of the exercise.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Note&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;        &lt;span class="kt"&gt;int&lt;/span&gt;       &lt;span class="s"&gt;`json:"id"`&lt;/span&gt;
    &lt;span class="n"&gt;Title&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="s"&gt;`json:"title"`&lt;/span&gt;
    &lt;span class="n"&gt;Content&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="s"&gt;`json:"content"`&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`json:"created_at"`&lt;/span&gt;
    &lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`json:"updated_at"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the API enough structure to feel real without becoming noisy. The current repo uses &lt;code&gt;Title&lt;/code&gt;, &lt;code&gt;Content&lt;/code&gt;, and timestamps, which already makes the responses clearer than a minimal &lt;code&gt;{ id, text }&lt;/code&gt; example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding an in-memory store
&lt;/h2&gt;

&lt;p&gt;To keep the project focused on Fiber itself, I used an in-memory store rather than a database. The store keeps notes in a &lt;code&gt;map[int]models.Note&lt;/code&gt;, tracks the next ID, and uses &lt;code&gt;sync.RWMutex&lt;/code&gt; so reads and writes stay safe. It exposes methods like &lt;code&gt;List&lt;/code&gt;, &lt;code&gt;GetById&lt;/code&gt;, &lt;code&gt;Create&lt;/code&gt;, &lt;code&gt;Update&lt;/code&gt;, and &lt;code&gt;Delete&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That choice was useful for two reasons.&lt;/p&gt;

&lt;p&gt;First, it made the project easier to understand. Second, it made the handler layer cleaner because the HTTP code does not need to care about how notes are stored. It only calls the store interface.&lt;/p&gt;

&lt;p&gt;Here is the kind of constructor I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewMemoryStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MemoryStore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;MemoryStore&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;nextId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here is the interface the handlers depend on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;NoteStore&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;
    &lt;span class="n"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;
    &lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That separation makes it easy to replace the memory store later with SQLite or PostgreSQL without rewriting the handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Registering routes
&lt;/h2&gt;

&lt;p&gt;Once the API group is created, the routes become straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;RegisterNoteRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api&lt;/span&gt; &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noteHandler&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;handlers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NoteHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;api&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/notes"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noteHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListNotes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noteHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetByID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noteHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noteHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/:id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;noteHandler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives a clean REST shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;GET /api/notes/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GET /api/notes/:id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;POST /api/notes/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PUT /api/notes/:id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DELETE /api/notes/:id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Writing the handlers
&lt;/h2&gt;

&lt;p&gt;The handlers are where Fiber starts to feel pleasant. Each method receives a &lt;code&gt;fiber.Ctx&lt;/code&gt;, reads route params or request bodies, validates input, calls the store, and returns JSON.&lt;/p&gt;

&lt;p&gt;Here is the create handler pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;NoteHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HasBody&lt;/span&gt;&lt;span class="p"&gt;()&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;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"request body is required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="n"&gt;CreateNoteInput&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Bind&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"invalid input"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;strings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TrimSpace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&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;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"title is required"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Content&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;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCreated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"note created successfully"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"data"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are a few things I like here.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;c.Bind().Body(&amp;amp;input)&lt;/code&gt; keeps request parsing readable. &lt;code&gt;strings.TrimSpace&lt;/code&gt; gives quick input cleanup. And &lt;code&gt;fiber.NewError(...)&lt;/code&gt; makes invalid states explicit.&lt;/p&gt;

&lt;p&gt;For route parameters, I convert the &lt;code&gt;id&lt;/code&gt; with &lt;code&gt;strconv.Atoi(c.Params("id"))&lt;/code&gt;. If the value is invalid, the handler returns a &lt;code&gt;400 Bad Request&lt;/code&gt;. If the note is not found, it returns a &lt;code&gt;404 Not Found&lt;/code&gt;. That small distinction already makes the API feel much more correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding middleware
&lt;/h2&gt;

&lt;p&gt;I also wanted the project to include middleware from day one, because middleware is one of the best ways to understand how requests flow through Fiber.&lt;/p&gt;

&lt;p&gt;The repo includes a simple custom logger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;SimpleLogger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="n"&gt;fiber&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%s %s %d %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&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;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This logs the HTTP method, path, status code, and execution time. It is small, but it already makes the server feel more like a real API. Alongside that, I added &lt;code&gt;recover.New()&lt;/code&gt; so panics are intercepted and forwarded through Fiber’s error pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this project was useful
&lt;/h2&gt;

&lt;p&gt;The biggest value of this project was not the notes domain itself. It was the repetition of the same backend fundamentals in a small space:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;structure the app&lt;/li&gt;
&lt;li&gt;register routes&lt;/li&gt;
&lt;li&gt;parse JSON&lt;/li&gt;
&lt;li&gt;validate input&lt;/li&gt;
&lt;li&gt;return proper status codes&lt;/li&gt;
&lt;li&gt;isolate storage from HTTP logic&lt;/li&gt;
&lt;li&gt;add middleware early&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the project stays small, each layer remains easy to inspect. And because the app still performs real CRUD operations, it feels much closer to real backend work than a “Hello World” example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;This version uses in-memory storage on purpose, which means data disappears when the server restarts. That is fine for learning, but the natural next steps are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add SQLite or PostgreSQL&lt;/li&gt;
&lt;li&gt;write unit tests for the store and handlers&lt;/li&gt;
&lt;li&gt;add request validation&lt;/li&gt;
&lt;li&gt;introduce JWT authentication&lt;/li&gt;
&lt;li&gt;add pagination and filtering&lt;/li&gt;
&lt;li&gt;dockerize the app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That progression turns a simple learning project into a stronger backend foundation without changing the core structure too much.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;Fiber made this project enjoyable because it stayed out of the way. I could focus on HTTP concepts, project structure, and clean handler logic instead of spending too much time wiring infrastructure.&lt;/p&gt;

&lt;p&gt;For anyone learning Go web development, building a small CRUD API like this is a great way to move from toy examples to something more realistic. And starting with notes is enough to learn the framework without getting buried in domain complexity.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>go</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>Building a Clean REST API in Go — No Frameworks, No Fuss</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Sat, 14 Mar 2026 15:36:12 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/building-a-clean-rest-api-in-go-no-frameworks-no-fuss-2dg1</link>
      <guid>https://dev.to/blamsa0mine/building-a-clean-rest-api-in-go-no-frameworks-no-fuss-2dg1</guid>
      <description>&lt;p&gt;Go's standard library is powerful enough to build a production-ready REST API without reaching for a framework. In this post, we'll walk through a small but well-structured &lt;strong&gt;Notes API&lt;/strong&gt; that covers all CRUD operations — and the architectural decisions behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;A simple API to manage notes, with five routes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Path&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/notes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a note&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/notes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all notes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/notes/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Get a note by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/notes/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update a note&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/notes/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a note&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;notes-api/
├── cmd/api/          # Entry point
├── internal/
│   ├── httpapi/      # HTTP handlers
│   ├── note/         # Model, service, storage interface
│   └── storage/      # In-memory implementation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three distinct layers: &lt;strong&gt;HTTP&lt;/strong&gt;, &lt;strong&gt;business logic&lt;/strong&gt;, and &lt;strong&gt;storage&lt;/strong&gt;. Each one&lt;br&gt;
knows nothing about the others except through interfaces.&lt;/p&gt;


&lt;h2&gt;
  
  
  The model
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Note&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;        &lt;span class="kt"&gt;int64&lt;/span&gt;     &lt;span class="s"&gt;`json:"id"`&lt;/span&gt;
    &lt;span class="n"&gt;Title&lt;/span&gt;     &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="s"&gt;`json:"title"`&lt;/span&gt;
    &lt;span class="n"&gt;Content&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;    &lt;span class="s"&gt;`json:"content"`&lt;/span&gt;
    &lt;span class="n"&gt;CreatedAt&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`json:"created_at"`&lt;/span&gt;
    &lt;span class="n"&gt;UpdatedAt&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="s"&gt;`json:"updated_at"`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Simple and flat. Timestamps are managed by the service, not by the caller.&lt;/p&gt;


&lt;h2&gt;
  
  
  The storage interface
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Store&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&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;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;GetByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Defining a &lt;code&gt;Store&lt;/code&gt; interface in the &lt;code&gt;note&lt;/code&gt; package (not in &lt;code&gt;storage&lt;/code&gt;) is a key decision: the business logic owns the contract, and the storage implementation satisfies it. This makes swapping backends (PostgreSQL, Redis, etc.) trivial later.&lt;/p&gt;


&lt;h2&gt;
  
  
  The service layer
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ErrInvalidTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"invalid title"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ErrNotFound&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"not found"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;""&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;Note&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;ErrInvalidTitle&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;     &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CreatedAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;UpdatedAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&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;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The service is where business rules live. Here it validates that the title is non-empty and stamps the timestamps before delegating to the store. Typed sentinel errors (&lt;code&gt;ErrInvalidTitle&lt;/code&gt;, &lt;code&gt;ErrNotFound&lt;/code&gt;) let the HTTP layer map them to the right status codes without leaking implementation details.&lt;/p&gt;


&lt;h2&gt;
  
  
  The in-memory storage
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;MemoryStorage&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt;     &lt;span class="n"&gt;sync&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RWMutex&lt;/span&gt;
    &lt;span class="n"&gt;notes&lt;/span&gt;  &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;
    &lt;span class="n"&gt;nextID&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A slice protected by a &lt;code&gt;sync.RWMutex&lt;/code&gt;. Read operations (&lt;code&gt;List&lt;/code&gt;, &lt;code&gt;GetByID&lt;/code&gt;) use &lt;code&gt;RLock&lt;/code&gt; to allow concurrent reads; write operations use &lt;code&gt;Lock&lt;/code&gt; for exclusive access. Note that the returned slice from &lt;code&gt;List&lt;/code&gt; is a copy — so callers can't accidentally mutate the internal state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;MemoryStorage&lt;/span&gt;&lt;span class="p"&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;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RLock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RUnlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Note&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nb"&gt;copy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notes&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;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The HTTP handler
&lt;/h2&gt;

&lt;p&gt;Routing is done with the standard &lt;code&gt;http.ServeMux&lt;/code&gt;. Two handler functions cover&lt;br&gt;
all five routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ServeMux&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/notes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handleNotes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/notes/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handleNoteByID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/notes&lt;/code&gt; handles &lt;code&gt;GET&lt;/code&gt; (list) and &lt;code&gt;POST&lt;/code&gt; (create). &lt;code&gt;/notes/&lt;/code&gt; catches everything&lt;br&gt;
with an ID suffix and dispatches on the method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;handleNoteByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;parseIDFromPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URL&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Invalid note ID"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Method&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodGet&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodPut&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;updateNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MethodDelete&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deleteNote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Method not allowed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusMethodNotAllowed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Service errors are translated to HTTP status codes in one place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;handleServiceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResponseWriter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrInvalidTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Invalid title"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusBadRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Is&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ErrNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Note not found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusNotFound&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Internal server error"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StatusInternalServerError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Wiring it all together
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewMemoryStore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;httpapi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Server is running on port 8080"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines of wiring. Each dependency is injected explicitly — no globals, no service locators.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try it out
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run ./cmd/api

&lt;span class="c"&gt;# Create&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/notes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"title": "Hello", "content": "World"}'&lt;/span&gt;

&lt;span class="c"&gt;# List&lt;/span&gt;
curl http://localhost:8080/notes

&lt;span class="c"&gt;# Get&lt;/span&gt;
curl http://localhost:8080/notes/1

&lt;span class="c"&gt;# Update&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; PUT http://localhost:8080/notes/1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"title": "Updated", "content": "New content"}'&lt;/span&gt;

&lt;span class="c"&gt;# Delete&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE http://localhost:8080/notes/1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No framework needed&lt;/strong&gt; for a simple CRUD API — &lt;code&gt;net/http&lt;/code&gt; is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interface in the consumer package&lt;/strong&gt; (&lt;code&gt;note.Store&lt;/code&gt; lives in &lt;code&gt;note&lt;/code&gt;, not&lt;code&gt;storage&lt;/code&gt;) keeps dependencies pointing inward.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typed sentinel errors&lt;/strong&gt; decouple business logic from HTTP concerns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sync.RWMutex&lt;/code&gt;&lt;/strong&gt; with defensive copies keeps the in-memory store safe under concurrency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency injection via constructors&lt;/strong&gt; makes the whole thing trivially testable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next natural step would be swapping &lt;code&gt;MemoryStorage&lt;/code&gt; for a real database (PostgreSQL with &lt;code&gt;pgx&lt;/code&gt;, for example) — and because of the &lt;code&gt;Store&lt;/code&gt; interface, &lt;code&gt;main.go&lt;/code&gt; is the only file that changes.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>go</category>
      <category>api</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building a Deterministic Password CLI in Go with Argon2id</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Thu, 12 Mar 2026 14:43:11 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/building-a-deterministic-password-cli-in-go-with-argon2id-59mp</link>
      <guid>https://dev.to/blamsa0mine/building-a-deterministic-password-cli-in-go-with-argon2id-59mp</guid>
      <description>&lt;p&gt;Most password generators focus on randomness.&lt;/p&gt;

&lt;p&gt;GitHub repo: &lt;a href="https://github.com/VincentCapek/argon2-password-cli" rel="noopener noreferrer"&gt;VincentCapek/argon2-password-cli&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This project takes a different route: it derives the &lt;strong&gt;same password every time&lt;/strong&gt; from a &lt;strong&gt;master password&lt;/strong&gt; and a &lt;strong&gt;label&lt;/strong&gt; such as &lt;code&gt;github&lt;/code&gt;, &lt;code&gt;gmail&lt;/code&gt;, or &lt;code&gt;bank&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That makes it a great little Go project for learning a few useful concepts at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;building a CLI with &lt;code&gt;flag&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;structuring a small Go codebase&lt;/li&gt;
&lt;li&gt;using Argon2id&lt;/li&gt;
&lt;li&gt;understanding deterministic derivation versus random generation&lt;/li&gt;
&lt;li&gt;shaping output with validation and character groups&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Instead of generating a brand-new password on every run, the CLI derives a reproducible one from two inputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a master password&lt;/li&gt;
&lt;li&gt;a label&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-master&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MyUltraSecret"&lt;/span&gt; &lt;span class="nt"&gt;-label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt; &lt;span class="nt"&gt;-length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Will always return the same password as long as the inputs and options stay the same.&lt;/p&gt;

&lt;p&gt;Change the label to &lt;code&gt;gmail&lt;/code&gt;, and the output changes too.&lt;/p&gt;

&lt;p&gt;That gives you a simple mental model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;same master + same label = same password&lt;/li&gt;
&lt;li&gt;same master + different label = different password&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why this is interesting
&lt;/h2&gt;

&lt;p&gt;When you first build a password generator, the obvious route is to use &lt;code&gt;crypto/rand&lt;/code&gt; and produce something fully random.&lt;/p&gt;

&lt;p&gt;That is still the right approach for many use cases.&lt;/p&gt;

&lt;p&gt;But a deterministic password CLI teaches a different lesson: you are not generating randomness for storage or one-time usage, you are &lt;strong&gt;deriving a stable result from a secret and a context&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is exactly why this kind of mini project is interesting from a learning perspective.&lt;/p&gt;

&lt;p&gt;It pushes you to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what a salt is&lt;/li&gt;
&lt;li&gt;why context matters&lt;/li&gt;
&lt;li&gt;how to separate CLI parsing from generation logic&lt;/li&gt;
&lt;li&gt;how to build a reproducible byte stream&lt;/li&gt;
&lt;li&gt;how to shape the final password so it remains usable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;

&lt;p&gt;The repository is intentionally small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main.go&lt;/code&gt; handles the CLI flags and builds the config&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;generator.go&lt;/code&gt; contains the derivation and password generation logic&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;go.mod&lt;/code&gt; defines the module and dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That small size is actually one of the project’s strengths. You can read the whole codebase in one sitting and still touch several important ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the generator works
&lt;/h2&gt;

&lt;p&gt;The generation pipeline is clean and easy to follow.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Parse CLI options
&lt;/h3&gt;

&lt;p&gt;The CLI accepts a master password, a label, a desired length, and options for lowercase, uppercase, digits, and symbols.&lt;/p&gt;

&lt;p&gt;That already makes the project practical while staying beginner-friendly.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Normalize the label
&lt;/h3&gt;

&lt;p&gt;Before using the label, the code normalizes it by trimming spaces and converting it to lowercase.&lt;/p&gt;

&lt;p&gt;That means values like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;github&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GitHub&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;GITHUB&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;all resolve to the same normalized label.&lt;/p&gt;

&lt;p&gt;This is a small but important detail, because it avoids accidental password drift caused by casing or whitespace differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Turn the label into a salt
&lt;/h3&gt;

&lt;p&gt;The normalized label is hashed with SHA-256, and the first 16 bytes are used as the salt.&lt;/p&gt;

&lt;p&gt;This is a neat design choice for a deterministic CLI:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the label remains human-friendly&lt;/li&gt;
&lt;li&gt;the salt is derived in a consistent way&lt;/li&gt;
&lt;li&gt;changing the label changes the salt, which changes the result&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Derive a seed with Argon2id
&lt;/h3&gt;

&lt;p&gt;The project then uses &lt;code&gt;argon2.IDKey(...)&lt;/code&gt; to derive a 32-byte seed from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the master password&lt;/li&gt;
&lt;li&gt;the derived salt&lt;/li&gt;
&lt;li&gt;the cost parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the cryptographic core of the project.&lt;/p&gt;

&lt;p&gt;Instead of directly turning the label into a password, the code first asks Argon2id to produce a strong derived key. That key becomes the seed for the next stage.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Build a deterministic byte stream
&lt;/h3&gt;

&lt;p&gt;From there, the project uses a custom &lt;code&gt;byteStream&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;combine the seed with a counter&lt;/li&gt;
&lt;li&gt;hash that combination with SHA-256&lt;/li&gt;
&lt;li&gt;use the resulting bytes as a deterministic stream&lt;/li&gt;
&lt;li&gt;increment the counter when more bytes are needed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a nice educational touch, because it makes the generation process easier to reason about than hiding everything behind a single opaque helper.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Build the password from character groups
&lt;/h3&gt;

&lt;p&gt;The code defines separate character sets for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lowercase letters&lt;/li&gt;
&lt;li&gt;uppercase letters&lt;/li&gt;
&lt;li&gt;digits&lt;/li&gt;
&lt;li&gt;symbols&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It first guarantees at least one character from each enabled group.&lt;/p&gt;

&lt;p&gt;Then it fills the rest of the password from the merged character set.&lt;/p&gt;

&lt;p&gt;Finally, it performs a deterministic shuffle so the password does not always start with the same kind of characters.&lt;/p&gt;

&lt;p&gt;That last part matters a lot. Without the shuffle, the output shape would be predictable even if the actual characters were not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small project, but a good set of lessons
&lt;/h2&gt;

&lt;p&gt;What I like about this repository is that it stays small without being trivial.&lt;/p&gt;

&lt;p&gt;It introduces several ideas that show up again and again in real projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validating input early&lt;/li&gt;
&lt;li&gt;separating config from logic&lt;/li&gt;
&lt;li&gt;composing small helper functions&lt;/li&gt;
&lt;li&gt;avoiding duplicated logic for character groups&lt;/li&gt;
&lt;li&gt;keeping the CLI thin and the generator focused&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words, this is not just a “security toy.” It is also a solid exercise in writing clean Go.&lt;/p&gt;

&lt;h2&gt;
  
  
  Random generation vs deterministic derivation
&lt;/h2&gt;

&lt;p&gt;This is probably the most important distinction in the whole project.&lt;/p&gt;

&lt;p&gt;A traditional password generator says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;give me a fresh random password right now&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This CLI says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;derive the same password again from a secret and a label&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference changes the whole architecture.&lt;/p&gt;

&lt;p&gt;If you want true randomness, &lt;code&gt;crypto/rand&lt;/code&gt; is the right tool.&lt;/p&gt;

&lt;p&gt;If you want reproducibility tied to a context, a derivation approach like this becomes much more interesting.&lt;/p&gt;

&lt;p&gt;That distinction is one of the reasons this project works so well as a learning exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example commands
&lt;/h2&gt;

&lt;p&gt;Generate a 20-character password for GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-master&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MyUltraSecret"&lt;/span&gt; &lt;span class="nt"&gt;-label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt; &lt;span class="nt"&gt;-length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-master&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MyUltraSecret"&lt;/span&gt; &lt;span class="nt"&gt;-label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"bank"&lt;/span&gt; &lt;span class="nt"&gt;-length&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;16 &lt;span class="nt"&gt;-symbols&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Increase the Argon2 cost parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-master&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MyUltraSecret"&lt;/span&gt; &lt;span class="nt"&gt;-label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt; &lt;span class="nt"&gt;-time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nt"&gt;-memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I would improve next
&lt;/h2&gt;

&lt;p&gt;If I were extending this project, I would probably add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hidden master-password input instead of passing it directly on the command line&lt;/li&gt;
&lt;li&gt;unit tests for deterministic output and validation rules&lt;/li&gt;
&lt;li&gt;profiles for site-specific password policies&lt;/li&gt;
&lt;li&gt;clipboard support&lt;/li&gt;
&lt;li&gt;a &lt;code&gt;cmd/&lt;/code&gt; layout if the project grows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is required for the project to be useful today, but each item would be a natural next step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I like projects like this because they stay focused.&lt;/p&gt;

&lt;p&gt;You do not need a framework, a database, or a large architecture to learn something meaningful. Sometimes a compact CLI is enough to explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go flags&lt;/li&gt;
&lt;li&gt;data validation&lt;/li&gt;
&lt;li&gt;helper types&lt;/li&gt;
&lt;li&gt;deterministic generation&lt;/li&gt;
&lt;li&gt;Argon2id integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a small but instructive Go project, building a deterministic password CLI like this is a very good exercise.&lt;/p&gt;

&lt;p&gt;It teaches both &lt;strong&gt;language fundamentals&lt;/strong&gt; and &lt;strong&gt;design trade-offs&lt;/strong&gt; in a way that stays concrete from start to finish.&lt;/p&gt;

</description>
      <category>go</category>
      <category>security</category>
      <category>cli</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Building SearchForge: a lightweight search interface with Go and Vue</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Wed, 11 Mar 2026 13:01:44 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/building-searchforge-a-lightweight-search-interface-with-go-and-vue-33di</link>
      <guid>https://dev.to/blamsa0mine/building-searchforge-a-lightweight-search-interface-with-go-and-vue-33di</guid>
      <description>&lt;p&gt;I like building products that are small, focused, and easy to reason about.&lt;/p&gt;

&lt;p&gt;That is exactly why I started &lt;strong&gt;SearchForge&lt;/strong&gt;: a local search application with a Go backend and a Vue 3 + TypeScript frontend, designed to feel like a modern SERP while staying simple enough to evolve quickly.&lt;/p&gt;

&lt;p&gt;The project is available on GitHub here: &lt;strong&gt;&lt;a href="https://github.com/VincentCapek/SearchForge" rel="noopener noreferrer"&gt;VincentCapek/SearchForge&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The goal was not to recreate a full search engine from day one.&lt;/p&gt;

&lt;p&gt;The goal was to build a clean foundation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a backend that can fetch and normalize results,&lt;/li&gt;
&lt;li&gt;a frontend that can render them in a familiar way,&lt;/li&gt;
&lt;li&gt;and an architecture flexible enough to support multiple search providers later.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;I wanted a project that sat at the intersection of backend structure, frontend UX, and search-oriented interfaces.&lt;/p&gt;

&lt;p&gt;Search is a great playground for that because even a “simple” search page forces you to think about several layers at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how results are fetched,&lt;/li&gt;
&lt;li&gt;how they are normalized,&lt;/li&gt;
&lt;li&gt;how errors are handled,&lt;/li&gt;
&lt;li&gt;how loading states feel,&lt;/li&gt;
&lt;li&gt;and how the UI stays readable when the data changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of starting with a huge stack, I deliberately chose a lightweight setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt; for the backend,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vue 3 + TypeScript&lt;/strong&gt; for the frontend,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vite&lt;/strong&gt; for fast iteration,&lt;/li&gt;
&lt;li&gt;and &lt;strong&gt;UnoCSS&lt;/strong&gt; for a design system that stays close to the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The architecture
&lt;/h2&gt;

&lt;p&gt;From the beginning, I wanted the codebase to be split by responsibility rather than mixed together in a single app layer.&lt;/p&gt;

&lt;p&gt;The project is organized around a few simple parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;search/&lt;/code&gt; contains the search fetching and parsing logic&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server/&lt;/code&gt; contains the HTTP server and route registration&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;server/api/&lt;/code&gt; contains the API handlers and JSON error helpers&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assets/&lt;/code&gt; contains the frontend application&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;utils/&lt;/code&gt; is where shared helpers live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation matters more than it seems.&lt;/p&gt;

&lt;p&gt;It means I can improve parsing logic without touching the UI, and I can redesign the frontend without rewriting the backend contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Go for the backend
&lt;/h2&gt;

&lt;p&gt;For this kind of project, Go felt like a natural fit.&lt;/p&gt;

&lt;p&gt;I wanted a backend that was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fast to run locally,&lt;/li&gt;
&lt;li&gt;easy to read,&lt;/li&gt;
&lt;li&gt;simple to deploy later,&lt;/li&gt;
&lt;li&gt;and not dependent on a heavy framework just to expose a few endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I used the standard library HTTP server and built a small router with dedicated handlers.&lt;/p&gt;

&lt;p&gt;That gave me a backend that stays explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a health endpoint,&lt;/li&gt;
&lt;li&gt;provider-specific search endpoints,&lt;/li&gt;
&lt;li&gt;and a small JSON layer for success and error responses.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One detail I particularly like is the generic handler factory.&lt;br&gt;&lt;br&gt;
Instead of repeating the same validation and response logic for every provider, I used a generic &lt;code&gt;makeSearchHandler&lt;/code&gt; function that wraps a search function and turns it into an HTTP handler.&lt;/p&gt;

&lt;p&gt;That keeps the API layer small while making it easy to plug in new providers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fetching and parsing search results
&lt;/h2&gt;

&lt;p&gt;The core of the project lives in the search layer.&lt;/p&gt;

&lt;p&gt;Right now, the backend already includes logic for multiple providers, including &lt;strong&gt;DuckDuckGo&lt;/strong&gt; and &lt;strong&gt;Brave&lt;/strong&gt;. The idea is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;build the search URL,&lt;/li&gt;
&lt;li&gt;fetch the HTML document,&lt;/li&gt;
&lt;li&gt;parse the relevant nodes,&lt;/li&gt;
&lt;li&gt;normalize the output,&lt;/li&gt;
&lt;li&gt;return a consistent JSON response to the frontend.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To do that, I used &lt;code&gt;goquery&lt;/code&gt;, which gives me a jQuery-like API for HTML parsing in Go.&lt;/p&gt;

&lt;p&gt;For DuckDuckGo, I parse result blocks, ignore ads, extract the title, URL, domain, and snippet, then normalize the URL before returning it.&lt;/p&gt;

&lt;p&gt;For Brave, I follow a similar flow, but with selectors adapted to Brave’s markup and a few extra fields such as site metadata.&lt;/p&gt;

&lt;p&gt;This is one of the most interesting parts of the project because scraping search results is never just “grab some HTML and done.”&lt;br&gt;&lt;br&gt;
You have to think about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;noisy markup,&lt;/li&gt;
&lt;li&gt;invalid links,&lt;/li&gt;
&lt;li&gt;duplicate results,&lt;/li&gt;
&lt;li&gt;result normalization,&lt;/li&gt;
&lt;li&gt;and how brittle selectors can become over time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Adding a small relevance filter
&lt;/h2&gt;

&lt;p&gt;I also added a lightweight blocklist layer.&lt;/p&gt;

&lt;p&gt;This lets me exclude domains that are not useful for the kind of result quality I want in the interface. It is a tiny feature, but it changes the feel of the output immediately.&lt;/p&gt;

&lt;p&gt;I like this kind of pragmatic layer because it shows that ranking and filtering are not always giant machine learning problems.&lt;br&gt;&lt;br&gt;
Sometimes, a simple handcrafted rule already makes the product feel better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing the frontend
&lt;/h2&gt;

&lt;p&gt;On the frontend side, I wanted the UI to stay clean and component-driven.&lt;/p&gt;

&lt;p&gt;The app is built with &lt;strong&gt;Vue 3&lt;/strong&gt;, &lt;strong&gt;TypeScript&lt;/strong&gt;, and &lt;strong&gt;Vite&lt;/strong&gt;, and split into small pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a search bar,&lt;/li&gt;
&lt;li&gt;a result list,&lt;/li&gt;
&lt;li&gt;individual result cards,&lt;/li&gt;
&lt;li&gt;and a composable dedicated to the search flow.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That composable is important.&lt;/p&gt;

&lt;p&gt;Instead of scattering state across the app, I grouped the core search state into a single place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the query,&lt;/li&gt;
&lt;li&gt;loading,&lt;/li&gt;
&lt;li&gt;error,&lt;/li&gt;
&lt;li&gt;results,&lt;/li&gt;
&lt;li&gt;and whether the user has already searched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That makes the UI easy to reason about because each visual state maps to a clear application state:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;initial state,&lt;/li&gt;
&lt;li&gt;loading state,&lt;/li&gt;
&lt;li&gt;empty state,&lt;/li&gt;
&lt;li&gt;error state,&lt;/li&gt;
&lt;li&gt;and result state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the kind of pattern I like in frontend projects: not complicated, just predictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I used UnoCSS
&lt;/h2&gt;

&lt;p&gt;I chose &lt;strong&gt;UnoCSS&lt;/strong&gt; because I wanted utility-first styling without turning the code into an unreadable wall of classes.&lt;/p&gt;

&lt;p&gt;The project defines shortcuts and theme tokens for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;layout,&lt;/li&gt;
&lt;li&gt;search fields,&lt;/li&gt;
&lt;li&gt;buttons,&lt;/li&gt;
&lt;li&gt;result cards,&lt;/li&gt;
&lt;li&gt;feedback panels,&lt;/li&gt;
&lt;li&gt;and typography.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means I can keep a consistent visual language while still moving fast.&lt;/p&gt;

&lt;p&gt;I also like that the design system already reflects the kind of interface I wanted: soft borders, muted text, clean spacing, and a search-first layout that feels familiar without being a copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping frontend and backend decoupled in development
&lt;/h2&gt;

&lt;p&gt;One practical choice that helped a lot was using &lt;strong&gt;Vite’s proxy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The frontend runs on its own dev server, while API and health requests are forwarded to the Go backend. That keeps the local workflow smooth:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run Go on port 8080,&lt;/li&gt;
&lt;li&gt;run Vite on port 5173,&lt;/li&gt;
&lt;li&gt;and let the frontend talk to the API without CORS headaches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is a small detail, but it makes iteration much faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I like about this project
&lt;/h2&gt;

&lt;p&gt;What I like most about SearchForge is that it already has the right shape.&lt;/p&gt;

&lt;p&gt;It is not trying to do everything.&lt;br&gt;&lt;br&gt;
It is trying to do the important things clearly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;separate concerns,&lt;/li&gt;
&lt;li&gt;normalize data at the backend layer,&lt;/li&gt;
&lt;li&gt;keep the frontend simple,&lt;/li&gt;
&lt;li&gt;and leave enough room for future expansion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are even traces of its earlier naming and evolution in the codebase, which I honestly like. It reminds me that good projects are rarely born “finished” — they become clearer as you build them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would improve next
&lt;/h2&gt;

&lt;p&gt;This project is already a strong base, but there are a lot of directions I want to explore next.&lt;/p&gt;

&lt;p&gt;A few obvious ones are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;adding more providers,&lt;/li&gt;
&lt;li&gt;introducing filters and engine selection,&lt;/li&gt;
&lt;li&gt;improving normalization across providers,&lt;/li&gt;
&lt;li&gt;storing search history,&lt;/li&gt;
&lt;li&gt;serving the frontend directly from Go in production,&lt;/li&gt;
&lt;li&gt;and refining the SERP UI further.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also want to keep improving the contract between backend and frontend so each provider can plug into the same interface cleanly.&lt;/p&gt;

&lt;p&gt;That is where this kind of project becomes really fun: once the foundation is stable, every new feature has a clear place to live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;SearchForge is the kind of project I enjoy building the most: small enough to move fast, but structured enough to grow into something more serious.&lt;/p&gt;

&lt;p&gt;It gave me a chance to work on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend API design,&lt;/li&gt;
&lt;li&gt;HTML parsing in Go,&lt;/li&gt;
&lt;li&gt;provider normalization,&lt;/li&gt;
&lt;li&gt;frontend state management,&lt;/li&gt;
&lt;li&gt;and design system thinking inside a Vue app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, it helped me build a product in layers instead of building everything at once.&lt;/p&gt;

&lt;p&gt;That is usually how I like to work:&lt;br&gt;
start with a clear backbone, make each part understandable, and leave room for the project to become more ambitious over time.&lt;/p&gt;

&lt;p&gt;If you are interested in lightweight full-stack projects, search interfaces, or pragmatic architecture, this kind of setup is a lot of fun to build.&lt;/p&gt;

&lt;p&gt;You can explore the codebase here: &lt;strong&gt;&lt;a href="https://github.com/VincentCapek/SearchForge" rel="noopener noreferrer"&gt;github.com/VincentCapek/SearchForge&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>go</category>
      <category>vue</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why ChatGPT Codex Became My Go-To Partner for Backend Business Logic</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Tue, 03 Mar 2026 16:35:22 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/why-chatgpt-codex-became-my-go-to-partner-for-backend-business-logic-2kla</link>
      <guid>https://dev.to/blamsa0mine/why-chatgpt-codex-became-my-go-to-partner-for-backend-business-logic-2kla</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I don’t use AI to "write CRUD faster." I use it where backend work is most expensive: &lt;strong&gt;business logic&lt;/strong&gt;, &lt;strong&gt;data invariants&lt;/strong&gt;, &lt;strong&gt;state transitions&lt;/strong&gt;, &lt;strong&gt;idempotency&lt;/strong&gt;, and &lt;strong&gt;regression-proof refactors&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Danger of Backend Bugs
&lt;/h2&gt;

&lt;p&gt;On the backend, speed is meaningless if you ship the wrong behavior. Unlike a UI bug, which is usually visible, a backend bug can be &lt;strong&gt;silent and devastating&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The wrong status is stored in the database.&lt;/li&gt;
&lt;li&gt;  A customer is charged the wrong amount.&lt;/li&gt;
&lt;li&gt;  A retry logic creates duplicate records.&lt;/li&gt;
&lt;li&gt;  A permission check is slightly too permissive.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These bugs generate support tickets and long-term distrust in the system. That’s why using an AI partner (like ChatGPT or Codex) is more about &lt;strong&gt;observability and precision&lt;/strong&gt; than just typing code.&lt;/p&gt;

&lt;p&gt;That’s why I started using &lt;strong&gt;ChatGPT Codex&lt;/strong&gt; as a &lt;em&gt;backend-focused pair programmer&lt;/em&gt;—not as a snippet generator.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Makes Business Logic Hard ?
&lt;/h2&gt;

&lt;p&gt;Business logic is rarely a single &lt;code&gt;if&lt;/code&gt; statement. It is a complex system of rules, priority between those rules, and &lt;strong&gt;invariants&lt;/strong&gt; (things that must never happen). The hard part isn't writing the code; it's stating the rule precisely and making it safe to evolve without breaking side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Codex helps most in backend projects
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Clarifying rules into a testable contract
&lt;/h3&gt;

&lt;p&gt;Before I implement, I use Codex to turn vague rules into something concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Given / When / Then&lt;/strong&gt; scenarios&lt;/li&gt;
&lt;li&gt;edge cases (“what happens if…?”)&lt;/li&gt;
&lt;li&gt;invariants and failure modes&lt;/li&gt;
&lt;li&gt;a decision table or scenario matrix&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal is not to “ask the model for the solution.”&lt;br&gt;
The goal is to &lt;strong&gt;remove ambiguity&lt;/strong&gt; and create a contract I can test and maintain.&lt;/p&gt;

&lt;p&gt;A simple prompt that works well:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are my backend reviewer. Turn this business rule into:
1) a short spec (in plain English),
2) a scenario matrix (nominal + edge cases),
3) unit tests (PHPUnit for PHP / pytest for Python),
4) implementation notes (invariants, idempotency, transactions, concurrency).
Business rule: &amp;lt;paste here&amp;gt;
Constraints: &amp;lt;DB, performance, API compatibility, etc.&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Surfacing the “invisible constraints” early
&lt;/h3&gt;

&lt;p&gt;The real backend gotchas usually live outside the feature description:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Idempotency&lt;/strong&gt; (duplicate requests, retries)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transactions&lt;/strong&gt; (partial failure)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;State machines&lt;/strong&gt; (allowed transitions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt; (race conditions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration contracts&lt;/strong&gt; (API, queues, webhooks)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codex is good at forcing those questions to the surface. Even if you decide “we don’t need this right now,” at least it becomes a &lt;strong&gt;conscious decision&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safer refactors across multiple files
&lt;/h3&gt;

&lt;p&gt;Backend refactors are scary because logic tends to spread:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controllers, services, repositories&lt;/li&gt;
&lt;li&gt;duplicated checks&lt;/li&gt;
&lt;li&gt;hidden coupling through shared helpers&lt;/li&gt;
&lt;li&gt;inconsistent error handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Codex helps me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;identify duplication and drift&lt;/li&gt;
&lt;li&gt;propose extractions (services / policies / use-cases)&lt;/li&gt;
&lt;li&gt;rename across files consistently&lt;/li&gt;
&lt;li&gt;produce a &lt;em&gt;reviewable patch&lt;/em&gt; instead of isolated snippets&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A concrete example: idempotent checkout with state transitions
&lt;/h2&gt;

&lt;p&gt;Let’s use a common scenario: &lt;strong&gt;checkout&lt;/strong&gt;.&lt;br&gt;
We want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a single charge even if the request is retried&lt;/li&gt;
&lt;li&gt;safe state transitions&lt;/li&gt;
&lt;li&gt;atomic DB changes&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Business rule (simplified)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;A checkout request includes an &lt;code&gt;idempotency_key&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If the same key is seen again, return the previously created result (no duplicate charge).&lt;/li&gt;
&lt;li&gt;Order states: &lt;code&gt;DRAFT -&amp;gt; PENDING_PAYMENT -&amp;gt; PAID&lt;/code&gt; (and no jumping backwards).&lt;/li&gt;
&lt;li&gt;If payment fails, mark as &lt;code&gt;PAYMENT_FAILED&lt;/code&gt; and keep the order consistent.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Example in PHP (Laravel-ish style)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;State transitions as explicit rules:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;DRAFT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'DRAFT'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;PENDING_PAYMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PENDING_PAYMENT'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;PAID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PAID'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="no"&gt;PAYMENT_FAILED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'PAYMENT_FAILED'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;canTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DRAFT&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PENDING_PAYMENT&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PENDING_PAYMENT&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PAID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PAYMENT_FAILED&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PAID&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
            &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PAYMENT_FAILED&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;in_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$allowed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$from&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Idempotency record (DB-backed):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// idempotency_keys table: key (unique), resource_type, resource_id, response_hash, created_at&lt;/span&gt;
&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IdempotencyStore&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;?IdempotencyRecord&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;IdempotencyRecord&lt;/span&gt; &lt;span class="nv"&gt;$record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Checkout service with transaction:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CheckoutService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;__construct&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;IdempotencyStore&lt;/span&gt; &lt;span class="nv"&gt;$idem&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;PaymentGateway&lt;/span&gt; &lt;span class="nv"&gt;$payments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;OrderRepository&lt;/span&gt; &lt;span class="nv"&gt;$orders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;CheckoutResult&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;idem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getCheckoutResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$existing&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;resourceId&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;DB&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;lockForUpdate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;canTransition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PENDING_PAYMENT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DomainException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Order cannot be checked out from status &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PENDING_PAYMENT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$payment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PAID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;idem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IdempotencyRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CheckoutResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;paid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$payment&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PaymentFailed&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;OrderStatus&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PAYMENT_FAILED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

                &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;idem&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;IdempotencyRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'order'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;CheckoutResult&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;failed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$order&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Example tests (PHPUnit)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_checkout_is_idempotent&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$service&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;makeService&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$r1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'abc'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$service&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotencyKey&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'abc'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r1&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$r2&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$r1&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$r2&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertEquals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;paymentGateway&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;chargeCount&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Same idea in Python (pytest)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_checkout_is_idempotent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment_gateway&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;r2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;checkout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idempotency_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;abc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;payment_gateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;charge_count&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How Codex fits into this workflow
&lt;/h2&gt;

&lt;p&gt;When I work on features like this, I use Codex for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spec shaping&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;“Rewrite this rule into a testable spec + scenario matrix.”&lt;/li&gt;
&lt;li&gt;“List invariants and failure modes.”&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Test-first scaffolding&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;“Generate PHPUnit/pytest tests for these scenarios.”&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Refactor safety&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;“Extract this into a domain service without changing behavior.”&lt;/li&gt;
&lt;li&gt;“Find duplicated business rules across the repo.”&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Review mode&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;“Act as a backend reviewer: point out hidden regressions, race conditions, missing cases.”&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Example “review prompt”:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Review this diff like a senior backend engineer.
Focus on:
- hidden behavior changes
- missing edge cases
- idempotency / retries
- transaction boundaries and consistency
- concurrency and locking
- error handling and observability
Then propose test cases to cover risks.
Diff: &amp;lt;paste diff or file paths&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Practical tips to get better results (without over-prompting)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Give constraints upfront:&lt;/strong&gt; DB, framework conventions, error handling style.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask for scenarios before code:&lt;/strong&gt; It reduces rework dramatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep business logic explicit:&lt;/strong&gt; state transitions, invariants, and idempotency rules should be visible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat outputs as drafts:&lt;/strong&gt; you still own the architecture and safety decisions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Codex as a reviewer:&lt;/strong&gt; it’s often most valuable when it challenges your assumptions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Codex doesn’t replace
&lt;/h2&gt;

&lt;p&gt;Codex is not a substitute for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;understanding your domain&lt;/li&gt;
&lt;li&gt;reviewing security implications&lt;/li&gt;
&lt;li&gt;deciding trade-offs (consistency vs latency, strict vs eventual)&lt;/li&gt;
&lt;li&gt;monitoring, logging, and alerting strategy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But it &lt;strong&gt;does&lt;/strong&gt; compress the feedback loop between “rule” and “correct behavior,” which is where backend teams spend a lot of time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;p&gt;For backend projects, “AI that writes code” is nice.&lt;br&gt;&lt;br&gt;
“AI that helps you &lt;strong&gt;ship safe business logic&lt;/strong&gt;” is a different level of usefulness.&lt;/p&gt;

&lt;p&gt;If you’re building backends in PHP or Python, try using Codex less like a generator and more like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a spec editor,&lt;/li&gt;
&lt;li&gt;a test designer,&lt;/li&gt;
&lt;li&gt;a refactor assistant,&lt;/li&gt;
&lt;li&gt;and a relentless reviewer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s where it pays off.&lt;/p&gt;

</description>
      <category>php</category>
      <category>python</category>
      <category>backend</category>
      <category>testing</category>
    </item>
    <item>
      <title>Laravel 13 + Inertia + Vue 3 + TypeScript: A Practical Upgrade Path from Laravel 12</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Sun, 01 Mar 2026 16:58:26 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/laravel-13-inertia-vue-3-typescript-a-practical-upgrade-path-from-laravel-12-eck</link>
      <guid>https://dev.to/blamsa0mine/laravel-13-inertia-vue-3-typescript-a-practical-upgrade-path-from-laravel-12-eck</guid>
      <description>&lt;p&gt;Laravel upgrades are often discussed as dry changelogs and release notes. To understand the real-world impact, I built a functional application using &lt;strong&gt;Laravel 13 + Inertia + Vue 3 + TypeScript&lt;/strong&gt; and compared the experience with &lt;strong&gt;Laravel 12&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;The experiment focused on shipping actual features: CRUD screens, validation, complex Eloquent queries, and relationships like reviews and polymorphic media.&lt;/p&gt;

&lt;p&gt;This article is a hands-on summary of what I built and what I learned.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on Timing:&lt;/strong&gt; At the time of this build, Laravel 13 was available as &lt;code&gt;13.x-dev&lt;/code&gt;. Expect minor ecosystem hiccups when using development branches.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. What Really Changes from Laravel 12 → Laravel 13
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The PHP Baseline Move
&lt;/h3&gt;

&lt;p&gt;The most significant "real-world" difference is the baseline: Laravel 13 pushes the minimum PHP version forward. While beneficial long-term, it immediately impacts your infrastructure, requiring updates to &lt;strong&gt;CI images&lt;/strong&gt;, &lt;strong&gt;Docker base images&lt;/strong&gt;, and hosting environments like &lt;strong&gt;Forge or Vapor&lt;/strong&gt;. In practice, the upgrade cost often lies more in your environment and dependencies than in your application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Attributes Become First-Class
&lt;/h3&gt;

&lt;p&gt;Laravel 13 leans heavily into &lt;strong&gt;PHP Attributes (&lt;code&gt;#[...]&lt;/code&gt;)&lt;/strong&gt; for configuration that previously lived in protected class properties. This makes classes more &lt;strong&gt;declarative&lt;/strong&gt;; you can open a model and immediately see its exports, permissions, and scopes at the top of the file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Inertia Stability
&lt;/h3&gt;

&lt;p&gt;One major advantage is that &lt;strong&gt;Inertia stays stable&lt;/strong&gt; during this transition. You can modernize your backend baseline while keeping your SPA-like frontend workflow—using &lt;code&gt;Inertia::render()&lt;/code&gt;, Vue components, and &lt;code&gt;useForm&amp;lt;T&amp;gt;()&lt;/code&gt;—exactly the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) Attributes (annotations) become first-class
&lt;/h3&gt;

&lt;p&gt;Laravel 13 leans into &lt;strong&gt;PHP Attributes&lt;/strong&gt; (&lt;code&gt;#[...]&lt;/code&gt;) for configuration that used to live in class properties.&lt;/p&gt;

&lt;p&gt;This is not about “saving lines of code”. It’s about making classes more &lt;strong&gt;declarative&lt;/strong&gt;: you open a model and immediately see what it allows/exports and which scopes exist.&lt;/p&gt;

&lt;p&gt;Here’s the exact style I used in the project (more on it later):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Fillable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Visible&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Scope&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Fillable(['name', 'price_cents', 'is_active'])]&lt;/span&gt;
&lt;span class="na"&gt;#[Visible(['id', 'name', 'price_cents', 'is_active'])]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[Scope]&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'like'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3) Inertia stays stable (and that’s a good thing)
&lt;/h3&gt;

&lt;p&gt;On the frontend side, Inertia doesn’t “change with Laravel 13”. The big value is that you can modernize the backend while keeping your SPA-like workflow the same:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;controller returns &lt;code&gt;Inertia::render(...)&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;pages are Vue components&lt;/li&gt;
&lt;li&gt;forms are powered by &lt;code&gt;useForm&amp;lt;T&amp;gt;()&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So your upgrade effort is &lt;strong&gt;concentrated in backend baseline + patterns&lt;/strong&gt;, not in “rewriting your frontend”.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project setup: Laravel 13 + Inertia + Vue 3 + TypeScript
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1) Composer + environment
&lt;/h3&gt;

&lt;p&gt;If you are on &lt;code&gt;13.x-dev&lt;/code&gt;, keep your &lt;code&gt;composer.json&lt;/code&gt; clean:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;minimum-stability&lt;/code&gt; and &lt;code&gt;prefer-stable&lt;/code&gt; must be &lt;strong&gt;top-level keys&lt;/strong&gt;, not inside &lt;code&gt;require&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;you may need &lt;code&gt;prefer-stable: true&lt;/code&gt; to avoid pulling dev versions for everything&lt;/li&gt;
&lt;li&gt;some packages may lag behind (I hit this with &lt;code&gt;laravel/tinker&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A minimal starting point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"require"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"php"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^8.3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"laravel/framework"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"13.x-dev"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"minimum-stability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"prefer-stable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;If Composer gets stuck on conflicts, I recommend a clean resolution pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; vendor composer.lock
composer &lt;span class="nb"&gt;install
&lt;/span&gt;php artisan &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2) Inertia server setup
&lt;/h3&gt;

&lt;p&gt;Install the Laravel adapter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require inertiajs/inertia-laravel &lt;span class="nt"&gt;-W&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the root template at &lt;code&gt;resources/views/app.blade.php&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html lang="en"&amp;gt;
&amp;lt;head&amp;gt;
  &amp;lt;meta charset="utf-8" /&amp;gt;
  &amp;lt;meta name="viewport" content="width=device-width, initial-scale=1.0" /&amp;gt;
  @vite(['resources/css/app.css', 'resources/js/app.ts'])
  @inertiaHead
&amp;lt;/head&amp;gt;
&amp;lt;body&amp;gt;
  @inertia
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create a middleware to share props (auth user, flash messages, etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Middleware&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Inertia\Middleware&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HandleInertiaRequests&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Middleware&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="nv"&gt;$rootView&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'app'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="k"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;share&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'auth'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'user'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="s1"&gt;'flash'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s1"&gt;'success'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="s1"&gt;'error'&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'error'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Laravel 13 uses &lt;code&gt;bootstrap/app.php&lt;/code&gt; middleware registration (no &lt;code&gt;Http\Kernel.php&lt;/code&gt;), so make sure your &lt;code&gt;HandleInertiaRequests&lt;/code&gt; is appended to the &lt;strong&gt;web&lt;/strong&gt; middleware group.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  3) Vue 3 + TypeScript + Vite
&lt;/h3&gt;

&lt;p&gt;Install dependencies (example with pnpm):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm add vue @inertiajs/vue3
pnpm add &lt;span class="nt"&gt;-D&lt;/span&gt; typescript vue-tsc @types/node @vitejs/plugin-vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;resources/js/app.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./bootstrap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;h&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createInertiaApp&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@inertiajs/vue3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolvePageComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;laravel-vite-plugin/inertia-helpers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="nf"&gt;createInertiaApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;resolvePageComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`./Pages/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.vue`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Pages/**/*.vue&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plugin&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;createApp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;render&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;h&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Feature 1: Products CRUD (Inertia + TS)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Generate the backend pieces
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan make:model Product &lt;span class="nt"&gt;-m&lt;/span&gt;
php artisan make:controller ProductController &lt;span class="nt"&gt;--resource&lt;/span&gt;
php artisan make:request StoreProductRequest
php artisan make:request UpdateProductRequest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migration (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Schema&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Blueprint&lt;/span&gt; &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;unsignedInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'price_cents'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'is_active'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Laravel 13-style model configuration with attributes
&lt;/h3&gt;

&lt;p&gt;Here’s the model again (this was the main “Laravel 13” part of the CRUD):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Builder&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Model&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Fillable&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Visible&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Database\Eloquent\Attributes\Scope&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="na"&gt;#[Fillable(['name', 'price_cents', 'is_active'])]&lt;/span&gt;
&lt;span class="na"&gt;#[Visible(['id', 'name', 'price_cents', 'is_active'])]&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Model&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;#[Scope]&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Builder&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'like'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Controller (pagination + search)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;App\Http\Controllers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Requests\StoreProductRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Http\Requests\UpdateProductRequest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;App\Models\Product&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Http\Request&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Inertia\Inertia&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductController&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Controller&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Request&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nv"&gt;$products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;when&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$query&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="c1"&gt;// attribute scope&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;paginate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withQueryString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Inertia&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Products/Index'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'products'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$products&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'filters'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$q&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Inertia&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Products/Create'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;StoreProductRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products.index'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Product created.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Inertia&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Products/Edit'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s1"&gt;'product'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;only&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'name'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'price_cents'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'is_active'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;UpdateProductRequest&lt;/span&gt; &lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$request&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;validated&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products.index'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Product updated.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;Product&lt;/span&gt; &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'products.index'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'success'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Product deleted.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  TypeScript-friendly Inertia pages
&lt;/h3&gt;

&lt;p&gt;On the Vue side, the most important thing is typing your form state and avoiding &lt;code&gt;v-model&lt;/code&gt; on readonly props.&lt;/p&gt;

&lt;p&gt;Example form type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ProductForm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;
  &lt;span class="na"&gt;price_cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
  &lt;span class="na"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useForm&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProductForm&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;price_cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;is_active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Feature 2: Reviews (Eloquent relations + Inertia pages)
&lt;/h2&gt;

&lt;p&gt;Once CRUD was working, I added &lt;strong&gt;reviews&lt;/strong&gt; to exercise Eloquent relationships.&lt;/p&gt;

&lt;h3&gt;
  
  
  Database shape
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;products&lt;/code&gt; 1—N &lt;code&gt;reviews&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;customers&lt;/code&gt; 1—N &lt;code&gt;reviews&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;unique constraint: &lt;code&gt;(product_id, customer_id)&lt;/code&gt; to ensure one review per customer per product&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That gives you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;relationship loading (&lt;code&gt;with('customer')&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;aggregates (&lt;code&gt;withCount&lt;/code&gt;, &lt;code&gt;withAvg&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;nested routes (&lt;code&gt;products/{product}/reviews&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Useful Eloquent aggregate
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;whereKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$product&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&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="nf"&gt;withCount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reviews'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withAvg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reviews'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rating'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of query is where Laravel’s ORM really shines: you get meaningful stats without hand-writing raw SQL everywhere.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature 3: Polymorphic media (one table, multiple parents)
&lt;/h2&gt;

&lt;p&gt;A polymorphic relation lets you attach media to multiple models (Product, Customer, Order...) using one &lt;code&gt;media&lt;/code&gt; table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;mediable_type&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mediable_id&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Migration helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;morphs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'mediable'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Media&lt;/code&gt; has &lt;code&gt;morphTo()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Product&lt;/code&gt; has &lt;code&gt;morphMany(Media::class, 'mediable')&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why it’s great:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you don’t need &lt;code&gt;product_images&lt;/code&gt;, &lt;code&gt;customer_avatars&lt;/code&gt;, &lt;code&gt;order_files&lt;/code&gt;...&lt;/li&gt;
&lt;li&gt;you get a uniform API in Eloquent (&lt;code&gt;$model-&amp;gt;media()-&amp;gt;create(...)&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Running the app (backend + frontend)
&lt;/h2&gt;

&lt;p&gt;Two terminals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;php artisan serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pnpm dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or a single command if you wired a &lt;code&gt;composer run dev&lt;/code&gt; script that starts both processes.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons learned (the honest part)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Laravel 13 dev branch is usable&lt;/strong&gt;, but expect dependency mismatches. If a package doesn’t support &lt;code&gt;^13&lt;/code&gt;, either drop it temporarily or use its dev branch.&lt;/li&gt;
&lt;li&gt;The biggest win of Laravel 13 in my build was &lt;strong&gt;declarative configuration with attributes&lt;/strong&gt;. It doesn’t rewrite your app, but it makes your codebase easier to read as it grows.&lt;/li&gt;
&lt;li&gt;Inertia is a perfect match for this kind of exploration: you can build real screens fast while keeping backend logic “Laravel-native”.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Next steps (if you want to push the ORM further)
&lt;/h2&gt;

&lt;p&gt;If you want to turn this into an even better Eloquent playground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;product variants (Product → ProductVariant, options/values, complex pivots)&lt;/li&gt;
&lt;li&gt;inventory &amp;amp; stock movements (transactions + aggregates)&lt;/li&gt;
&lt;li&gt;multi-tenant scoping (global scopes + tenant_id)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>vue</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Laravel Scheduler in Production: Why I Use It (and How I Make It Reliable)</title>
      <dc:creator>A0mineTV</dc:creator>
      <pubDate>Thu, 26 Feb 2026 12:49:20 +0000</pubDate>
      <link>https://dev.to/blamsa0mine/laravel-scheduler-in-production-why-i-use-it-and-how-i-make-it-reliable-43df</link>
      <guid>https://dev.to/blamsa0mine/laravel-scheduler-in-production-why-i-use-it-and-how-i-make-it-reliable-43df</guid>
      <description>&lt;p&gt;Scheduled tasks are easy—until they aren’t. The first time an invoice isn’t sent, a sync silently stops, or a report runs twice and crashes your server, you realize scheduled work isn’t just "ops trivia." It’s a &lt;strong&gt;product risk&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For a long time, I treated scheduling as a server concern by adding lines to a crontab. But that led to major pain points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Tasks were configured outside the codebase (not versioned or reviewable).&lt;/li&gt;
&lt;li&gt;  Differences between staging and production ("it works on my server").&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Overlapping jobs&lt;/strong&gt; because a previous run didn't finish.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Double executions&lt;/strong&gt; when the app scaled to multiple instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laravel’s Scheduler solves the real problem: &lt;strong&gt;Governance&lt;/strong&gt;. It turns scheduling into something you can read, review, deploy, and reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: the server triggers, Laravel orchestrates
&lt;/h2&gt;

&lt;p&gt;In production, you only need &lt;strong&gt;one&lt;/strong&gt; cron entry on your server:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;run &lt;code&gt;php artisan schedule:run&lt;/code&gt; every minute&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That’s it.&lt;/p&gt;

&lt;p&gt;From this point, Laravel decides which tasks are due. Your schedule becomes part of your application code, shifting it from a "mystery ops config" to versioned application behavior.&lt;/p&gt;

&lt;p&gt;This shifts scheduling from “mystery ops config” to &lt;strong&gt;versioned application behavior&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I prefer Laravel Scheduler over “pure crontab”
&lt;/h2&gt;

&lt;p&gt;I’m not anti-cron. Cron is great at one thing: triggering commands at a regular cadence.&lt;/p&gt;

&lt;p&gt;The issues start when cron becomes the place where &lt;em&gt;business-critical workflows&lt;/em&gt; live. Because then you’re maintaining system behavior in a place that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;is not part of pull requests&lt;/li&gt;
&lt;li&gt;can’t be code-reviewed the same way&lt;/li&gt;
&lt;li&gt;varies between environments&lt;/li&gt;
&lt;li&gt;becomes messy over time (and nobody wants to touch it)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Laravel Scheduler fixes that by letting me express scheduling as &lt;strong&gt;intent&lt;/strong&gt;, not cron syntax.&lt;/p&gt;

&lt;p&gt;Instead of thinking “what is the crontab line for weekdays at 02:00?”, I can encode the intent directly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every day at 2 AM&lt;/li&gt;
&lt;li&gt;in the correct timezone&lt;/li&gt;
&lt;li&gt;never overlap&lt;/li&gt;
&lt;li&gt;only run once even with multiple servers&lt;/li&gt;
&lt;li&gt;keep output for auditing&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My “production defaults”: make tasks safe by design
&lt;/h2&gt;

&lt;p&gt;Almost every important scheduled task in my projects includes two safety guarantees:&lt;/p&gt;

&lt;h3&gt;
  
  
  1) No overlaps
&lt;/h3&gt;

&lt;p&gt;If a job is still running, the next scheduled tick should &lt;strong&gt;not&lt;/strong&gt; start another copy.&lt;/p&gt;

&lt;p&gt;That’s what &lt;code&gt;withoutOverlapping()&lt;/code&gt; gives you.&lt;/p&gt;

&lt;p&gt;Overlaps cause the most annoying class of bugs: &lt;em&gt;duplicates&lt;/em&gt;. Duplicate emails. Duplicate invoices. Duplicate exports. Duplicate API calls. Duplicate side effects.&lt;/p&gt;

&lt;h3&gt;
  
  
  2) One execution across multiple servers
&lt;/h3&gt;

&lt;p&gt;When an app scales horizontally, cron runs on every instance by default.&lt;/p&gt;

&lt;p&gt;Without protection, the same scheduled task can run N times.&lt;/p&gt;

&lt;p&gt;That’s what &lt;code&gt;onOneServer()&lt;/code&gt; is for.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you’ve ever scaled to two servers and suddenly saw doubled notifications… you only need that incident once to adopt &lt;code&gt;onOneServer()&lt;/code&gt; forever.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  A concrete example (Carbon-friendly snippet)
&lt;/h2&gt;

&lt;p&gt;Let’s say you generate a daily report at 2 AM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="kn"&gt;use&lt;/span&gt; &lt;span class="nc"&gt;Illuminate\Support\Facades\Schedule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nc"&gt;Schedule&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'reports:daily'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dailyAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'02:00'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Europe/Paris'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;onOneServer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;withoutOverlapping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;sendOutputTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;storage_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logs/schedule-reports.log'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What I like about this code is that it reads like a checklist of business intent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Daily at 02:00&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Correct timezone&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Single run across instances&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;No overlap&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Output preserved for auditing&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Observability: don’t “trust” schedules—prove they run
&lt;/h2&gt;

&lt;p&gt;A scheduled task that fails silently is worse than one that fails loudly.&lt;/p&gt;

&lt;p&gt;So I treat scheduled work like I treat anything business-critical: it needs traceability.&lt;/p&gt;

&lt;p&gt;At minimum, I want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;output persisted somewhere (&lt;code&gt;sendOutputTo&lt;/code&gt;, &lt;code&gt;appendOutputTo&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;a way to audit the configured schedule (&lt;code&gt;php artisan schedule:list&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;visibility in logs/monitoring when something breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the task is critical (payments, invoices, notifications), I go a step further and connect failures to alerts.&lt;/p&gt;

&lt;p&gt;The point is not “more tooling”. The point is &lt;strong&gt;shorter time to detect&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scheduler is an orchestrator, not a worker
&lt;/h2&gt;

&lt;p&gt;Here’s a rule that saved me multiple times:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The scheduler should trigger work, not &lt;em&gt;be&lt;/em&gt; the work.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If something can be slow, fragile, or dependent on external services, I don’t want it to run as one long synchronous command inside &lt;code&gt;schedule:run&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Instead, I schedule a command that dispatches a job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the schedule stays quick and predictable&lt;/li&gt;
&lt;li&gt;the heavy work runs in the queue&lt;/li&gt;
&lt;li&gt;retries and failures are handled properly&lt;/li&gt;
&lt;li&gt;monitoring becomes easier&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is also how you avoid minute-based drift when tasks take longer than expected.&lt;/p&gt;




&lt;h2&gt;
  
  
  The “SQL vs PHP” equivalent in scheduling
&lt;/h2&gt;

&lt;p&gt;I use a similar separation of concerns as with data transformations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cron/Laravel schedule: &lt;strong&gt;when&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;job/command: &lt;strong&gt;what&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;queue workers: &lt;strong&gt;how it executes reliably&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When those responsibilities are mixed, maintenance becomes painful.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common pitfalls (that look fine until production)
&lt;/h2&gt;

&lt;p&gt;I’ve seen these issues repeatedly:&lt;/p&gt;

&lt;h3&gt;
  
  
  “It works locally but not in prod”
&lt;/h3&gt;

&lt;p&gt;Often the schedule is correct, but the server isn’t actually triggering it (missing cron entry, wrong PHP path, wrong user).&lt;/p&gt;

&lt;h3&gt;
  
  
  Overlaps
&lt;/h3&gt;

&lt;p&gt;The task runs every minute, but takes 2 minutes. Now you have two copies running. Then three.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-instance duplicates
&lt;/h3&gt;

&lt;p&gt;Scaling from 1 to 2 servers doubles everything—emails, webhooks, cleanup jobs.&lt;/p&gt;

&lt;h3&gt;
  
  
  No logs, no audit trail
&lt;/h3&gt;

&lt;p&gt;You’re guessing whether it ran. Guessing is not a strategy.&lt;/p&gt;




&lt;h2&gt;
  
  
  A quick production checklist
&lt;/h2&gt;

&lt;p&gt;If I’m shipping scheduled tasks, I want to answer these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is there exactly one trigger cron on the server ?&lt;/li&gt;
&lt;li&gt;Can this task overlap? If yes, how do I prevent it ?&lt;/li&gt;
&lt;li&gt;Can this task run on multiple servers ? If yes, how do I enforce single execution ?&lt;/li&gt;
&lt;li&gt;Where do I see output ?&lt;/li&gt;
&lt;li&gt;If it fails, how do I find out quickly ?&lt;/li&gt;
&lt;li&gt;Should this be a queued job instead ?&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Laravel Scheduler doesn’t just “schedule tasks”.&lt;/p&gt;

&lt;p&gt;It gives you a way to treat scheduled work like &lt;strong&gt;real application behavior&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;versioned&lt;/li&gt;
&lt;li&gt;reviewable&lt;/li&gt;
&lt;li&gt;predictable&lt;/li&gt;
&lt;li&gt;safer in production&lt;/li&gt;
&lt;li&gt;easier to audit and troubleshoot&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And once you’ve dealt with a silent failure or duplicated side effects in production, that shift is worth a lot.&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>devops</category>
      <category>backend</category>
    </item>
  </channel>
</rss>
