<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Codango® / Codango.Com</title>
	<atom:link href="https://codango.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://codango.com</link>
	<description></description>
	<lastBuildDate>Tue, 02 Jun 2026 20:28:19 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9</generator>

<image>
	<url>https://codango.com/wp-content/uploads/cropped-faviconpng-32x32.png</url>
	<title>Codango® / Codango.Com</title>
	<link>https://codango.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>[Boost]</title>
		<link>https://codango.com/boost-4/</link>
					<comments>https://codango.com/boost-4/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 20:28:19 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/boost-4/</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.amazonaws.com2Fuploads2Forganization2Fprofile_image2F33692Fc86918f8-76a9-4b01-accf-cc257f9ee56f-qiG7CH.png" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" />Build a Food-Ordering Chatbot in 30 Minutes by .NET 10 and Azure Duc Nguyen Thanh ・ May 12 #webdev #ai #azure #dotnet]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.amazonaws.com2Fuploads2Forganization2Fprofile_image2F33692Fc86918f8-76a9-4b01-accf-cc257f9ee56f-qiG7CH.png" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" /><div class="ltag__link">
  <a href="https://dev.to/ngtduc693" class="ltag__link__link">
<div class="ltag__link__pic">
      <img decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2100289%2F51194f82-686a-45b7-bf02-372f819385b3.jpg" alt="ngtduc693" />
    </div>
<p>  </p></a><br />
  <a href="https://dev.to/ngtduc693/build-a-food-ordering-chatbot-in-30-minutes-by-net-10-and-azure-14df" class="ltag__link__link">
<div class="ltag__link__content">
<h2>Build a Food-Ordering Chatbot in 30 Minutes by .NET 10 and Azure</h2>
<h3>Duc Nguyen Thanh ・ May 12</h3>
<div class="ltag__link__taglist">
        <span class="ltag__link__tag">#webdev</span><br />
        <span class="ltag__link__tag">#ai</span><br />
        <span class="ltag__link__tag">#azure</span><br />
        <span class="ltag__link__tag">#dotnet</span>
      </div>
</div>
<p>  </p></a>
</div>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/boost-4/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Bruno Mock Server: Alternativen und Tools im Vergleich</title>
		<link>https://codango.com/bruno-mock-server-alternativen-und-tools-im-vergleich/</link>
					<comments>https://codango.com/bruno-mock-server-alternativen-und-tools-im-vergleich/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 10:23:25 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/bruno-mock-server-alternativen-und-tools-im-vergleich/</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fassets.apidog.com2Fblog-next2F20262F062Fimage-38-nCiFt5-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" />Bruno ist ein leichter, Git-nativer Open-Source-API-Client. Er eignet sich gut, um Requests als Dateien zu versionieren, Requests auszuführen und Tests zu schreiben. Was Bruno nicht liefert: einen integrierten Mock-Server für <a class="more-link" href="https://codango.com/bruno-mock-server-alternativen-und-tools-im-vergleich/">Continue reading <span class="screen-reader-text">  Bruno Mock Server: Alternativen und Tools im Vergleich</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fassets.apidog.com2Fblog-next2F20262F062Fimage-38-nCiFt5-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p>Bruno ist ein leichter, Git-nativer Open-Source-API-Client. Er eignet sich gut, um Requests als Dateien zu versionieren, Requests auszuführen und Tests zu schreiben. Was Bruno nicht liefert: einen integrierten Mock-Server für Endpunkte, die noch nicht existieren. Wenn Sie einen Bruno-Mock-Server suchen, brauchen Sie daher entweder ein externes Tool, einen selbst geschriebenen Stub-Server oder einen spezifikationsgesteuerten Mock aus Ihrer OpenAPI-Datei.</p>
<p><a href="https://apidog.com/?utm_source=dev.to&amp;utm_medium=wanda&amp;utm_content=n8n-post-automation" class="crayons-btn crayons-btn--primary">Apidog noch heute ausprobieren</a>
</p>
<p>Kurz gesagt: Bruno kann Anfragen senden und Assertions ausführen, aber keine gefälschten Endpunkte bereitstellen, die Beispielantworten zurückgeben. Für Mocking müssen Sie den Mock außerhalb von Bruno betreiben.</p>
<h2>
<p>  Warum Sie einen Mock-Server benötigen<br />
</p></h2>
<p>Ein Mock-Server liefert definierte Antworten für API-Endpunkte, die noch nicht implementiert, instabil oder schwer reproduzierbar sind. Das ist besonders nützlich für:</p>
<ul>
<li>
<strong>Parallele Entwicklung:</strong> Frontend-, Mobile- und Backend-Teams arbeiten gegen denselben API-Vertrag.</li>
<li>
<strong>Fehlerpfad-Tests:</strong> Sie können gezielt <code>429</code>, <code>500</code>, <code>503</code> oder fehlerhafte Payloads auslösen.</li>
<li>
<strong>Demos und Prototypen:</strong> Workflows funktionieren ohne Live-Backend, Datenbank oder Zugangsdaten.</li>
<li>
<strong>Stabile CI:</strong> Tests hängen nicht von Staging-Ausfällen, Rate Limits oder instabilen Testdaten ab.</li>
</ul>
<p>Typische Szenarien:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Szenario</th>
<th>Was der Mock zurückgibt</th>
<th>Warum es sonst schwierig ist</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ratenbegrenzung erreicht</td>
<td>
<code>429</code> + <code>Retry-After</code> Header</td>
<td>Backend drosselt selten auf Anfrage</td>
</tr>
<tr>
<td>Serverausfall</td>
<td>
<code>500</code> / <code>503</code>
</td>
<td>Staging lässt sich nicht gezielt für Tests unterbrechen</td>
</tr>
<tr>
<td>Langsame Antwort</td>
<td>Verzögerte Antwort</td>
<td>Reale Latenz ist schwer reproduzierbar</td>
</tr>
<tr>
<td>Leeres Ergebnis</td>
<td>
<code>200</code> mit <code>[]</code>
</td>
<td>Hängt vom aktuellen Datenzustand ab</td>
</tr>
<tr>
<td>Fehlerhaftes Payload</td>
<td>Body ohne erforderliches Feld</td>
<td>Backend-Validierung verhindert solche Fälle normalerweise</td>
</tr>
</tbody>
</table>
</div>
<h2>
<p>  Hat Bruno einen Mock-Server?<br />
</p></h2>
<p>Nein. Bruno konzentriert sich auf:</p>
<ul>
<li>Requests senden</li>
<li>Collections als einfache Dateien organisieren</li>
<li>Tests und Assertions ausführen</li>
<li>API-Workflows Git-nativ verwalten</li>
</ul>
<p>Es gibt aber keine native Funktion, die eine gespeicherte Anfrage in einen laufenden Stub-Endpunkt verwandelt.</p>
<p>In der Praxis nutzen Bruno-Teams meist eine dieser zwei Optionen:</p>
<ol>
<li>
<p><strong>Externes Mocking-Tool verwenden</strong></p>
<p>Zum Beispiel Mockoon, WireMock, Prism oder json-server. Sie definieren dort die Antworten und verweisen Bruno auf die Mock-URL.</p>
</li>
<li>
<p><strong>Eigenen Stub-Server schreiben</strong></p>
<p>Zum Beispiel mit Express, Flask oder FastAPI. Das ist für wenige Endpunkte schnell, wird bei wachsender API aber pflegeintensiv.</p>
</li>
</ol>
<p>Beide Wege funktionieren. Beide bedeuten aber auch: Der Mock lebt außerhalb Ihrer Bruno-Collection.</p>
<h2>
<p>  Beispiel: Minimaler Stub-Server mit Express<br />
</p></h2>
<p>Wenn Sie nur einen schnellen lokalen Mock brauchen, reicht ein kleiner Express-Server:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight javascript"><code><span class="k">import</span> <span class="nx">express</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">express</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">const</span> <span class="nx">app</span> <span class="o">=</span> <span class="nf">express</span><span class="p">();</span>
<span class="nx">app</span><span class="p">.</span><span class="nf">use</span><span class="p">(</span><span class="nx">express</span><span class="p">.</span><span class="nf">json</span><span class="p">());</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">/users/:id</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">({</span>
    <span class="na">id</span><span class="p">:</span> <span class="nx">req</span><span class="p">.</span><span class="nx">params</span><span class="p">.</span><span class="nx">id</span><span class="p">,</span>
    <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Max Mustermann</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">email</span><span class="p">:</span> <span class="dl">"</span><span class="s2">max@example.com</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">created_at</span><span class="p">:</span> <span class="k">new</span> <span class="nc">Date</span><span class="p">().</span><span class="nf">toISOString</span><span class="p">()</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="dl">"</span><span class="s2">/rate-limit</span><span class="dl">"</span><span class="p">,</span> <span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">set</span><span class="p">(</span><span class="dl">"</span><span class="s2">Retry-After</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">60</span><span class="dl">"</span><span class="p">);</span>
  <span class="nx">res</span><span class="p">.</span><span class="nf">status</span><span class="p">(</span><span class="mi">429</span><span class="p">).</span><span class="nf">json</span><span class="p">({</span>
    <span class="na">error</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Too Many Requests</span><span class="dl">"</span>
  <span class="p">});</span>
<span class="p">});</span>

<span class="nx">app</span><span class="p">.</span><span class="nf">listen</span><span class="p">(</span><span class="mi">3000</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">"</span><span class="s2">Mock server running at http://localhost:3000</span><span class="dl">"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre>
</div>
<p>Danach können Sie in Bruno gegen diese URL testen:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>GET http://localhost:3000/users/123
</code></pre>
</div>
<p>Das ist praktisch für kleine Tests. Der Nachteil: Sobald sich Ihre API-Spezifikation ändert, müssen Sie den Stub manuell aktualisieren.</p>
<h2>
<p>  Die Kosten von nachträglich hinzugefügtem Mocking<br />
</p></h2>
<p>Eine separate Mock-Schicht ist machbar, erzeugt aber schnell Reibung:</p>
<ul>
<li>
<strong>Drift:</strong> Spezifikation, Bruno-Requests und Mock-Definitionen liegen an verschiedenen Stellen.</li>
<li>
<strong>Setup-Aufwand:</strong> Neue Teammitglieder müssen zusätzlich ein Mocking-Tool installieren und konfigurieren.</li>
<li>
<strong>Manuelle Payloads:</strong> Beispielantworten für Felder, Statuscodes und Edge Cases müssen gepflegt werden.</li>
<li>
<strong>Statische Daten:</strong> Einfache Stubs geben oft immer dieselbe Antwort zurück und verdecken dadurch Fehler.</li>
</ul>
<p>Wenn ein Endpunkt geändert wird, müssen Sie typischerweise mehrere Artefakte aktualisieren:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>OpenAPI-Spezifikation
        ↓
Bruno-Request
        ↓
Externer Mock / Stub-Server
</code></pre>
</div>
<p>Vergessen Sie eine Stelle, testen Sie gegen veraltetes Verhalten.</p>
<p>Eine detailliertere Einordnung finden Sie in dieser Analyse zur <a href="https://apidog.com/de/blog/bruno-alternative-all-in-one-api-platform?utm_source=dev.to&amp;utm_medium=wanda&amp;utm_content=n8n-post-automation">Bruno-Alternative als All-in-One-API-Plattform</a>.</p>
<h2>
<p>  Besser: Mock-Server aus Ihrer OpenAPI-Spezifikation generieren<br />
</p></h2>
<p>Der sauberere Ansatz ist, den Mock aus dem API-Vertrag zu erzeugen, den Sie ohnehin pflegen.</p>
<p><a href="https://apidog.com/?utm_source=dev.to&amp;utm_medium=wanda&amp;utm_content=n8n-post-automation">Apidog</a> kann eine OpenAPI-Spezifikation importieren oder direkt verwalten und daraus einen funktionierenden Mock-Server generieren. Dadurch verwenden Design, Mocking, Tests und Dokumentation dieselbe Quelle.</p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.apidog.com%2Fblog-next%2F2026%2F06%2Fimage-38.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.apidog.com%2Fblog-next%2F2026%2F06%2Fimage-38.png" alt="" width="800" height="421" /></a></p>
<p>Der Workflow sieht so aus:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>OpenAPI-Spezifikation
        ↓
Mock-Server
        ↓
Frontend / Mobile App / Tests
</code></pre>
</div>
<p>Der Vorteil: Wenn sich das Schema ändert, ändert sich auch der Mock auf Basis derselben Definition.</p>
<h2>
<p>  Was ein spezifikationsgesteuerter Mock praktisch löst<br />
</p></h2>
<p>Ein aus der Spezifikation generierter Mock reduziert manuelle Arbeit:</p>
<ul>
<li>
<strong>Schema-basierte Antworten:</strong> Felder und Typen werden aus OpenAPI gelesen.</li>
<li>
<strong>Plausiblere Daten:</strong> Ein <code>email</code>-Feld kann wie eine E-Mail aussehen, ein <code>created_at</code>-Feld wie ein Datum.</li>
<li>
<strong>Dynamische Responses:</strong> Antworten können anhand des Schemas generiert werden, statt immer denselben statischen Body zurückzugeben.</li>
<li>
<strong>Weniger Setup:</strong> Sie müssen keinen separaten Server schreiben oder hosten.</li>
<li>
<strong>Synchronisierung:</strong> Aktualisieren Sie die Spezifikation, bleibt der Mock am selben Vertrag ausgerichtet.</li>
</ul>
<p>Wenn Ihr Team Git-zentriert arbeitet, bleibt die Spezifikation weiterhin diff-fähig und reviewbar. Das passt gut zu einem <a href="https://apidog.com/de/blog/git-native-api-workflow?utm_source=dev.to&amp;utm_medium=wanda&amp;utm_content=n8n-post-automation">Git-nativen API-Workflow</a>. Weitere Beispiele finden Sie in den <a href="https://apidog.com/de/blog/api-mocking-use-cases?utm_source=dev.to&amp;utm_medium=wanda&amp;utm_content=n8n-post-automation">Anwendungsfällen für API-Mocking</a>.</p>
<h2>
<p>  Kurzanleitung: Von OpenAPI zur Mock-URL<br />
</p></h2>
<p>So erstellen Sie einen Mock aus einer bestehenden Spezifikation:</p>
<ol>
<li>
<p><strong>OpenAPI-Spezifikation importieren</strong></p>
<p>Importieren Sie Ihre OpenAPI- oder Swagger-Datei oder verwenden Sie eine Spezifikations-URL.</p>
</li>
<li>
<p><strong>Endpunkt öffnen</strong></p>
<p>Jeder importierte Endpunkt enthält bereits Pfad, Methode, Parameter, Request Body und Response-Schema.</p>
</li>
<li>
<p><strong>Mock-URL kopieren</strong></p>
<p>Apidog stellt eine Mock-URL bereit, ohne dass Sie einen separaten Server deployen müssen.</p>
</li>
<li>
<p><strong>Request senden</strong></p>
<p>Rufen Sie die Mock-URL aus Ihrem Frontend, Ihrer mobilen App, Ihrer Testsuite oder Bruno auf.</p>
</li>
<li>
<p><strong>Fehlerfälle ergänzen</strong></p>
<p>Definieren Sie bei Bedarf spezielle Antworten wie <code>429</code>, <code>500</code>, leere Listen oder fehlerhafte Payloads.</p>
</li>
</ol>
<p>Beispielhafter Testablauf:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>1. Backend-Endpunkt ist noch nicht fertig
2. OpenAPI-Schema definiert GET /users/{id}
3. Mock-URL wird generiert
4. Frontend ruft Mock-URL auf
5. UI- und Fehlerpfade können sofort getestet werden
</code></pre>
</div>
<h2>
<p>  Wann Bruno plus externes Mocking ausreicht<br />
</p></h2>
<p>Sie müssen nicht immer auf eine spezifikationsgesteuerte Lösung wechseln. Bruno plus externes Mocking reicht oft aus, wenn:</p>
<ul>
<li>Sie nur ein oder zwei Endpunkte lokal simulieren.</li>
<li>Ein statischer Stub genügt.</li>
<li>Sie bereits Mockoon, WireMock oder Prism einsetzen.</li>
<li>Ihr Team bewusst bei Brunos dateibasierten Collections bleiben möchte.</li>
<li>Die zusätzliche Mock-Wahrheitsquelle keine Wartungsprobleme verursacht.</li>
</ul>
<p>Der Trade-off ist klar:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Ansatz</th>
<th>Vorteil</th>
<th>Nachteil</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bruno + externes Mocking</td>
<td>Leichtgewichtig, flexibel</td>
<td>Mock muss separat gepflegt werden</td>
</tr>
<tr>
<td>Eigener Stub-Server</td>
<td>Maximale Kontrolle</td>
<td>Manuelle Implementierung und Wartung</td>
</tr>
<tr>
<td>OpenAPI-basierter Mock</td>
<td>Eine Quelle der Wahrheit</td>
<td>Breitere Plattform im Workflow</td>
</tr>
</tbody>
</table>
</div>
<p>Wählen Sie den Ansatz danach, wie groß Ihre API ist und wie oft sich Endpunkte ändern.</p>
<h2>
<p>  FAQ<br />
</p></h2>
<h3>
<p>  Hat Bruno einen integrierten Mock-Server?<br />
</p></h3>
<p>Nein. Bruno ist ein API-Client zum Senden von Anfragen und Ausführen von Tests. Einen nativen Mock-Server gibt es nicht. Für Mocking verwenden Sie ein externes Tool oder schreiben einen eigenen Stub-Server.</p>
<h3>
<p>  Was ist der einfachste Weg, Mocking zu einem Bruno-ähnlichen Workflow hinzuzufügen?<br />
</p></h3>
<p>Der einfachste nachhaltige Weg ist ein Mock aus Ihrer OpenAPI-Spezifikation. Dadurch müssen Sie Mock-Definitionen nicht separat pflegen und können Design, Mocking, Tests und Dokumentation auf denselben Vertrag stützen.</p>
<h3>
<p>  Kann ich Bruno weiterverwenden und trotzdem einen Mock-Server nutzen?<br />
</p></h3>
<p>Ja. Sie können Bruno weiter als Client verwenden und Requests gegen Mockoon, WireMock, Prism, json-server, einen eigenen Stub-Server oder eine generierte Mock-URL senden. Der Unterschied liegt darin, wo Sie die Mock-Daten pflegen.</p>
<h3>
<p>  Wann lohnt sich ein spezifikationsgesteuerter Mock?<br />
</p></h3>
<p>Wenn Ihre API wächst, mehrere Teams parallel arbeiten oder Sie viele Fehlerpfade testen müssen. Dann spart eine zentrale OpenAPI-basierte Quelle meist mehr Zeit als getrennte Mock-Definitionen.</p>
<p>Wenn die Pflege einer separaten Mock-Schicht mehr kostet, als sie spart, testen Sie einen spezifikationsgesteuerten Mock. Importieren Sie Ihre OpenAPI-Datei in <a href="https://apidog.com/?utm_source=dev.to&amp;utm_medium=wanda&amp;utm_content=n8n-post-automation">Apidog</a> und erzeugen Sie daraus eine Mock-URL, ohne einen zusätzlichen Server zu hosten.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/bruno-mock-server-alternativen-und-tools-im-vergleich/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>PagerDuty’s 83% Stock Drop Since 2019 and What We Learned from It in 2026</title>
		<link>https://codango.com/pagerdutys-83-stock-drop-since-2019-and-what-we-learned-from-it-in-2026/</link>
					<comments>https://codango.com/pagerdutys-83-stock-drop-since-2019-and-what-we-learned-from-it-in-2026/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 10:21:00 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/pagerdutys-83-stock-drop-since-2019-and-what-we-learned-from-it-in-2026/</guid>

					<description><![CDATA[There’s nothing like a good old fashioned budget review to remind you what you’re actually spending money on… and how much of it. PagerDuty is one of those tools that <a class="more-link" href="https://codango.com/pagerdutys-83-stock-drop-since-2019-and-what-we-learned-from-it-in-2026/">Continue reading <span class="screen-reader-text">  PagerDuty’s 83% Stock Drop Since 2019 and What We Learned from It in 2026</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>There’s nothing like a good old fashioned budget review to remind you what you’re actually spending money on… and how much of it.</p>
<p>PagerDuty is one of those tools that makes you do a double-take when you open the invoice. You ask yourself: “Is this the monthly or the annual figure?” while wondering if either would even be acceptable.</p>
<p>This collective eyebrow raise isn’t because PagerDuty is bad software, but because the world around it changed while PagerDuty stayed the same.</p>
<p>To understand what happened, you have to look at the bigger picture; the world around PagerDuty that evolved while it remained static. Let’s see how their story unfolds.</p>
<h2>
<p>  Act I: Setting the scene<br />
</p></h2>
<p>PagerDuty IPO’d in 2019 and by 2021 it was trading around $34. It came onto the scene like the big brother of incident response, knowing it had nailed a problem that everyone else was either ignoring or still trying to solve. PagerDuty was reliable; the kind of tool you bought to show off that you had your operational act together.</p>
<p>Fast-forward to 2025/2026 and it’s suffered a multi-year collapse with a <a href="https://simplywall.st/stocks/us/software/nyse-pd/pagerduty/news/has-pagerduty-pd-become-a-potential-opportunity-after-prolon" rel="noopener noreferrer">73% decline over 5 years</a>. For a while, the market was all about PagerDuty, but then it did what it normally does: it changed its mind.</p>
<p>In November 2025, PagerDuty’s stock plummeted <a href="https://www.ainvest.com/news/pagerduty-pd-sudden-24-stock-drop-assessing-overreaction-long-term-investment-implications-2511/" rel="noopener noreferrer">24% in a single day</a>. Their team had waved a small victory flag while publishing their Q3 results with GAAP profitability for the second straight quarter, which was great news! Until they pulled back their revenue guidance because of customers cutting seats and watching their budgets.</p>
<p>Sure, the financial housekeeping seemed cleaner, as their margins were improving on the surface and EPS was behaving as it should. But then the curtain was drawn: they lowered their growth expectations and analysts went straight for the jugular. Suddenly, everyone was wondering whether PagerDuty could even keep its revenue engine running at the pace Wall Street expected.</p>
<p><em><strong>If your <a href="https://allquiet.app/on-call" rel="noopener noreferrer">on-call alerts</a> dropped this hard, you’d file an incident report.</strong></em></p>
<h2>
<p>  Act II: Enterprise pricing in a market that’s lost confidence<br />
</p></h2>
<p>I don’t want to sugar-coat it. PagerDuty’s pricing has always been aspirational. They’ve positioned themselves as enterprise-grade, while CTOs increasingly see them as enterprise-inflated. It’s the kind of pricing that assumes you have a huge team, an even bigger budget and an extremely forgiving CFO (or one who’s asleep on the job).</p>
<p>This approach worked for years, though. Enterprise pricing was part of the deal for DevOps starter packs, but then the pendulum swung. Seat-based pricing is now under more pressure than ever before. <a href="https://www.ainvest.com/news/pagerduty-pd-sudden-24-stock-drop-assessing-overreaction-long-term-investment-implications-2511/" rel="noopener noreferrer">Analysts even said</a> that seat license compression and slowing ARR growth were key factors in PagerDuty’s revenue slowdown.</p>
<p>In other words, customers were trimming their usage rather than expanding it. But why do that when you can let the system take care of the groundwork for you? A huge amount of alert handling is just repetition: checking if the alert is real, gathering context, matching it to other signals, rinse and repeat. But a solid system can filter out the junk and enrich alerts with the right information, then connect related events and trigger the fixes. You instantly cut down on noise, along with <a href="https://www.secure.com/blog/cybersecurity/automated-security-investigations" rel="noopener noreferrer">70% of security investigations</a>.</p>
<p>You can’t replace your analyst, but you absolutely can clear the fog so they can focus on alerts that matter.</p>
<h3>
<p>  Act III: Do you really need the Cadillac?<br />
</p></h3>
<p>While PagerDuty was busy expanding its platform, <a href="https://allquiet.app/blog/top-incident-management-solutions" rel="noopener noreferrer">modern alternatives</a> were slowly creeping up on it with better open-source tools, more mature cloud providers and bootstrapped SaaS tools. Suddenly, you could get 80-90% of PagerDuty’s core functionality without paying enterprise prices.</p>
<p>This wasn’t a symptom of PagerDuty being copied by competitors, but incident response itself becoming a solved problem. Cloud-native alerting like AWS, GCP and Azure had matured dramatically, while open-source tools like Alertmanager continued to get better and better. They were offering alerting pipelines that, five years ago, would’ve looked like something from Futurama.</p>
<p>Incident response suddenly had flat pricing, transparent billing, no per-seat penalties and support teams that actually answered emails. Meanwhile, PagerDuty was still innovating. They rolled out <a href="https://www.zacks.com/stock/news/2385360/pagerduty-declines-16-ytd-should-you-buy-the-stock-on-the-dip" rel="noopener noreferrer">AI-driven automation</a> and workflow orchestrations, and even reported that 825 customers were spending $100k+ ARR in Q3 2024.</p>
<p>But does innovation erase the pricing gap for lean teams?</p>
<p>Big, fat no. And that’s when migrations start to happen.</p>
<h3>
<p>  Act IV: The not-so-quiet migration<br />
</p></h3>
<p>If you want to really understand the shift in the market, go to the belly of the beast. Look at Slack channels, look at Reddit threads–go to the very places where engineers actually tell the truth. You’ll see common themes emerging in the DevOps community and messages like:</p>
<ul>
<li>“We switched and nothing broke.”</li>
<li>“We were paying for features we never used.”</li>
<li>“We were punished with higher prices for growing.”</li>
<li>“We replaced it with a lightweight SaaS and cloud-native tool.”</li>
</ul>
<p>The reality is that companies weren’t fighting back or even arguing with PagerDuty. They simply… moved on. They realized that they, in fact, didn’t need the Cadillac. They turned away from longer sales cycles and SMB-weakness, towards cheaper software that worked well and didn’t require a whole board meeting to approve.</p>
<p>Act V: The lean, mean, incident response machines</p>
<p>Call the news stations, everyone; the bootstrapped competitors are having their moment in the sun. Incident response is entering a new era and the winners aren’t the biggest, shiniest platforms with features pouring out of the box. They’re the ones offering:</p>
<ul>
<li>Flat pricing</li>
<li>Transparent billing</li>
<li>Faster support</li>
<li>No need to justify their existence every fiscal year</li>
<li>Alignment with modern engineering teams</li>
<li>No pressure to satisfy Wall Street.</li>
</ul>
<p>PagerDuty’s <a href="https://www.investing.com/news/company-news/pagerduty-stock-hits-52week-low-at-1394-usd-93CH-4104355" rel="noopener noreferrer">52-week low of $13.94</a> in 2025, a 34.67% YoY drop, wasn’t a failure, but a sign of customer and investor uncertainty. It proved that the old model of “enterprise pricing for everyone” simply wasn’t the go-to anymore. And unless companies are willing to evolve in line with that reality, they’ll fade into the background of those that thrive in the next decade.</p>
<p>Final act: What this means for the future of incident response<br />
The incident response market is going through a big personality change. It’s moving from enterprise-first to developer-first, with big platforms, big bundles and big invoices. PagerDuty is a case study in what happens when pricing strategy diverges from customer value. If the bill keeps climbing but the value doesn’t, customers won’t bother complaining about it. They’ll just leave.</p>
<p>And here’s the twist: even though multiple DCF analyses suggest PagerDuty is undervalued by 50-55%, investors still question its growth prospects. The future of incident response will be defined by tools that don’t overcharge or overcomplicate, and simply do what they say they’ll do without making you expense a limb.</p>
<p>So are you paying for product or public market overhead?</p>
<p>An <a href="https://finance.yahoo.com/quote/PD/" rel="noopener noreferrer">82.8% stock-drop</a> isn’t something investors will bat their eyelids at. A fall that hard forces customers to rethink what they’re actually paying for: the product? The innovation? The reliability? Or just the cost of being a public company with slowing growth and rising pressure?</p>
<p>And not just customers, but CTOs too. They’re evaluating whether they’re using the features they pay for and if the pricing model makes sense for how their teams work today. They want to know if the vendor is stable enough to bet their operations on and what the ROI is compared to the alternatives (or just building it themselves).</p>
<p>Your on-call budget shouldn’t be keeping you up at night. You’ve got enough alerts for that. But if you want a quieter life with less interruptions, <a href="https://allquiet.app/" rel="noopener noreferrer">talk to us today</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/pagerdutys-83-stock-drop-since-2019-and-what-we-learned-from-it-in-2026/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>JWT Lifecycle vs. Secret Rotation: Which is More Secure?</title>
		<link>https://codango.com/jwt-lifecycle-vs-secret-rotation-which-is-more-secure/</link>
					<comments>https://codango.com/jwt-lifecycle-vs-secret-rotation-which-is-more-secure/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 10:16:01 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/jwt-lifecycle-vs-secret-rotation-which-is-more-secure/</guid>

					<description><![CDATA[&#x2139;&#xfe0f; What You Will Learn in This Post I will delve into the two main pillars of the JWT security model: token lifecycle management and secret key rotation strategies. I&#8217;ll <a class="more-link" href="https://codango.com/jwt-lifecycle-vs-secret-rotation-which-is-more-secure/">Continue reading <span class="screen-reader-text">  JWT Lifecycle vs. Secret Rotation: Which is More Secure?</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<blockquote>
<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2139.png" alt="ℹ" class="wp-smiley" style="height: 1em; max-height: 1em;" /> What You Will Learn in This Post</strong></p>
<p>I will delve into the two main pillars of the JWT security model: token lifecycle management and secret key rotation strategies. I&#8217;ll explain, based on my own experiences and concrete examples, how these two approaches should be used together in real-world scenarios.</p>
</blockquote>
<h2>
<p>  Introduction: Fundamentals of JWT Security<br />
</p></h2>
<p>When API security is discussed, JWT (JSON Web Token) is almost always one of the first solutions on the table. I, too, have relied on JWTs in many projects, especially when building microservice architectures or a stateless authentication structure. However, simply using JWTs isn&#8217;t enough; ensuring their security is only possible with proper lifecycle management and secret key rotation.</p>
<p>Just taking and using a token means missing the bigger picture. The truth is, how a JWT is created, how long it&#8217;s valid, and how the secret key that signs it is managed are vital for your overall system security. In this post, I will explain, based on my own experiences, what these two critical components – the JWT&#8217;s lifecycle and the rotation of its signing secret key – mean and how they should be implemented.</p>
<h2>
<p>  JWT Lifecycle Management: Why Short-Lived Tokens?<br />
</p></h2>
<p>The primary purpose of JWTs is to exchange information securely between client and server without maintaining state. Once signed, these tokens cannot be changed or revoked (at least not with a standard mechanism) until their expiration. While this feature makes them very useful, it also carries a significant security risk: if a JWT is compromised, it can be used by malicious actors for its entire validity period.</p>
<p>For this reason, my philosophy has always been to use short-lived access tokens. I typically keep an <code>access_token</code>&#8216;s lifespan between 15 minutes and 1 hour. Alongside this, I use a longer-lived <code>refresh_token</code> to maintain a continuous session without disrupting the user experience. For example, when designing operator screens in a production ERP, we sometimes extended <code>refresh_token</code>s up to 7 days to avoid constant token renewal requests, but this always requires careful balancing.</p>
<h3>
<p>  Token Revocation and Blacklisting Mechanisms<br />
</p></h3>
<p>While short-lived <code>access_token</code>s minimize damage in case of token theft, they are not &#8220;forever&#8221; secure. Sometimes, I might need to urgently terminate a user&#8217;s session or revoke a token before its expiration. This is where blacklisting comes into play.</p>
<p>In my approaches, I blacklist <code>access_token</code>s by storing them in a fast, distributed cache system like Redis. When a user logs out or suspicious activity is detected, I add the relevant <code>access_token</code> to Redis and mark it with a TTL (Time-To-Live) equal to its validity period. With every incoming request, I first check if the token is in Redis. If the token is blacklisted, the request is rejected.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="c1"># Example of a JWT blacklist check in a FastAPI application
</span><span class="kn">from</span> <span class="n">redis</span> <span class="kn">import</span> <span class="n">Redis</span>
<span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">timedelta</span>

<span class="c1"># Redis connection
</span><span class="n">redis_client</span> <span class="o">=</span> <span class="nc">Redis</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="sh">'</span><span class="s">localhost</span><span class="sh">'</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">6379</span><span class="p">,</span> <span class="n">db</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">blacklist_token</span><span class="p">(</span><span class="n">token</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">expires_delta</span><span class="p">:</span> <span class="n">timedelta</span><span class="p">):</span>
    <span class="sh">"""</span><span class="s">Blacklists the token in Redis.</span><span class="sh">"""</span>
    <span class="n">redis_client</span><span class="p">.</span><span class="nf">setex</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">blacklist:</span><span class="si">{</span><span class="n">token</span><span class="si">}</span><span class="sh">"</span><span class="p">,</span> <span class="n">expires_delta</span><span class="p">.</span><span class="nf">total_seconds</span><span class="p">(),</span> <span class="sh">"</span><span class="s">1</span><span class="sh">"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">is_token_blacklisted</span><span class="p">(</span><span class="n">token</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
    <span class="sh">"""</span><span class="s">Checks if the token is blacklisted.</span><span class="sh">"""</span>
    <span class="k">return</span> <span class="n">redis_client</span><span class="p">.</span><span class="nf">exists</span><span class="p">(</span><span class="sa">f</span><span class="sh">"</span><span class="s">blacklist:</span><span class="si">{</span><span class="n">token</span><span class="si">}</span><span class="sh">"</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span>

<span class="c1"># Usage example
# access_token_expires = timedelta(minutes=15)
# blacklist_token(my_token, access_token_expires)
# if is_token_blacklisted(my_token):
#     raise HTTPException(status_code=401, detail="Token revoked")
</span></code></pre>
</div>
<p>This approach, of course, has a cost. An additional Redis check on every request adds a slight load to performance. Furthermore, in distributed systems, the high availability and performance of the Redis cluster itself become critical. Once, because I didn&#8217;t configure Redis OOM eviction policy settings correctly, our blacklist service couldn&#8217;t correctly store tokens under sudden load, and some revoked tokens became valid again for a short period. Such edge cases demonstrate how detailed system management needs to be.</p>
<h2>
<p>  JWT Secret Key Rotation: How Often Do You Change Your Key?<br />
</p></h2>
<p>The security of JWTs largely depends on the secrecy of the secret key (or private key) with which they are signed. If this secret key is leaked, a malicious actor can sign any JWT and bypass authorization in your system as they wish. This is precisely why secret key rotation, i.e., regularly changing the key, is an indispensable security practice.</p>
<p>In my projects, I typically change secret keys every 30 to 90 days. This rotation process is managed through automated scripts or a CI/CD pipeline. However, this is just a number; the actual frequency varies depending on the system&#8217;s sensitivity, potential attack vectors, and legal requirements. For instance, I opted for more frequent rotation in the backend of one of my financial calculator side products.</p>
<h3>
<p>  Strategies for Seamless Key Transition<br />
</p></h3>
<p>While secret key rotation sounds simple, doing it without interruption in a running system is challenging. When you switch to a new secret, tokens signed with the old secret might still be valid, and instantly invalidating them would negatively impact user experience. To solve this problem, I usually use &#8220;key rollover&#8221; strategies.</p>
<p>In this strategy, multiple secret keys are kept active simultaneously. New tokens are signed with the newest secret, while incoming tokens can be validated with both new and old secrets. This ensures a smooth transition until the old tokens expire.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="c1"># A simple example: Managing multiple secret keys
# In a real system, these keys would be retrieved from a Key Management System (KMS) or
# a secure vault.
</span>
<span class="n">ACTIVE_SECRETS</span> <span class="o">=</span> <span class="p">[</span>
    <span class="sh">"</span><span class="s">super-secret-key-current</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">super-secret-key-old-1</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">super-secret-key-old-2</span><span class="sh">"</span>
<span class="p">]</span>

<span class="k">def</span> <span class="nf">verify_jwt_with_multiple_keys</span><span class="p">(</span><span class="n">token</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">dict</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">secret</span> <span class="ow">in</span> <span class="n">ACTIVE_SECRETS</span><span class="p">:</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="n">payload</span> <span class="o">=</span> <span class="n">jwt</span><span class="p">.</span><span class="nf">decode</span><span class="p">(</span><span class="n">token</span><span class="p">,</span> <span class="n">secret</span><span class="p">,</span> <span class="n">algorithms</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">HS256</span><span class="sh">"</span><span class="p">])</span>
            <span class="k">return</span> <span class="n">payload</span>
        <span class="k">except</span> <span class="n">jwt</span><span class="p">.</span><span class="n">ExpiredSignatureError</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">401</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Token expired</span><span class="sh">"</span><span class="p">)</span>
        <span class="k">except</span> <span class="n">jwt</span><span class="p">.</span><span class="n">InvalidTokenError</span><span class="p">:</span>
            <span class="k">continue</span> <span class="c1"># Could not verify with this secret, try another
</span>    <span class="k">raise</span> <span class="nc">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">401</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="sh">"</span><span class="s">Invalid token signature</span><span class="sh">"</span><span class="p">)</span>
</code></pre>
</div>
<p>If asymmetric encryption (RSA, ECDSA) is used, this process can be managed more elegantly via JWKS (JSON Web Key Set) endpoints. The server publishes its public keys at a JWKS endpoint, and clients or other services validate tokens using these public keys. When a new key set is introduced, the JWKS endpoint is updated, and old keys continue to be published for a while. This is a structure I often see in protocols like OpenID Connect. I can delve into this topic in more detail in my [related: SSO integration with OpenID Connect] post.</p>
<h2>
<p>  Comparison from a Security Perspective: Who Complements Whom?<br />
</p></h2>
<p>JWT lifecycle management and secret key rotation are two different mechanisms that address different security risks. Instead of comparing them, it&#8217;s a much more accurate approach to view them as complementary elements.</p>
<ul>
<li>
<p><strong>JWT Lifecycle Management (Short-Lived Tokens):</strong> This approach <strong>limits the damage caused by a compromised token.</strong> If an <code>access_token</code> is stolen, its short lifespan narrows the window of time during which a malicious actor can use it. For example, a 15-minute token carries much less risk than a 24-hour token if stolen. Token revocation (blacklisting) further provides the ability to stop this damage even earlier.</p>
</li>
<li>
<p><strong>Secret Key Rotation:</strong> This approach, on the other hand, <strong>limits the damage that would occur if the signing key itself were compromised.</strong> If your secret key is leaked, an attacker can generate valid tokens at will. Regular rotation restricts the validity period of a leaked key and forces the attacker to constantly obtain new keys, which increases the cost of the attack. While working on an internal banking platform, I experienced how critical such key rotations are and how important it is to automate them without manual intervention.</p>
</li>
</ul>
<p>In short, short-lived tokens are a defense against token theft, while secret rotation is a defense against the theft of the signing key. In an ideal JWT security architecture, both should work together in harmony. Neglecting one significantly weakens the security provided by the other.</p>
<h2>
<p>  Integration and Challenges in My Application Architecture<br />
</p></h2>
<p>In my experience, using both short-lived tokens and secret rotation together has always been a balancing act. For example, when developing an operator screen based on <code>FastAPI</code> and <code>Vue.js</code> for a production ERP, I set <code>access_token</code>s to 30 minutes and <code>refresh_token</code>s to 3 days. <code>refresh_token</code>s were stored in a database (PostgreSQL) and replaced with a new <code>refresh_token</code> on each use, with the old token being revoked. This ensures that even if a <code>refresh_token</code> is stolen, it is single-use.</p>
<p>On the secret rotation side, I retrieved the keys from a Key Management System (KMS) or as an environment variable from my <code>systemd unit</code>s. In a client project, we used <code>systemd timer</code>s to automate key rotation. A script running every 90 days would generate a new key, save it to the KMS, and restart the relevant service.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># systemd timer example (jwt-key-rotation.timer)</span>
<span class="o">[</span>Unit]
<span class="nv">Description</span><span class="o">=</span>Run JWT Key Rotation every 90 days

<span class="o">[</span>Timer]
<span class="nv">OnCalendar</span><span class="o">=</span><span class="k">*</span>-<span class="k">*</span><span class="nt">-01</span> 03:00:00 <span class="c"># Runs on the 1st of every month at 03:00</span>
<span class="nv">Persistent</span><span class="o">=</span><span class="nb">true</span>

<span class="o">[</span>Install]
<span class="nv">WantedBy</span><span class="o">=</span>timers.target

<span class="c"># systemd service example (jwt-key-rotation.service)</span>
<span class="o">[</span>Unit]
<span class="nv">Description</span><span class="o">=</span>Rotate JWT Signing Key
<span class="nv">After</span><span class="o">=</span>network.target

<span class="o">[</span>Service]
<span class="nv">Type</span><span class="o">=</span>oneshot
<span class="nv">ExecStart</span><span class="o">=</span>/usr/local/bin/rotate_jwt_key.sh
<span class="nv">User</span><span class="o">=</span>jwt_rotator
<span class="nv">Group</span><span class="o">=</span>jwt_rotator
<span class="nv">StandardOutput</span><span class="o">=</span>journal
<span class="nv">StandardError</span><span class="o">=</span>journal

<span class="o">[</span>Install]
<span class="nv">WantedBy</span><span class="o">=</span>multi-user.target
</code></pre>
</div>
<p>This <code>rotate_jwt_key.sh</code> script would generate a new secret, securely save it, and reload my <code>Nginx</code> reverse proxy or <code>FastAPI</code> services. However, caution is needed here. Last month, in a similar script, I wrote <code>sleep 360</code> to wait for the new key to be distributed, only to find the script was OOM-killed after exceeding <code>cgroup memory.high</code> limits. I then switched to a <code>polling-wait</code> mechanism, meaning the script would wait in a loop until it verified that the service had indeed restarted and was using the new key. Such small errors can lead to unexpected outages in large systems.</p>
<p>Securely distributing secrets in distributed systems is also a separate challenge. In <code>Docker Compose</code>-based deployments, it&#8217;s safer to use tools like <code>Docker secrets</code> or <code>Vault</code> instead of passing secrets as environment variables. For my own side product&#8217;s backend, for my <code>Docker Compose</code> services running on a VPS, I adopted a kind of &#8220;bare-metal + container hybrid&#8221; deployment model by encrypting environment files and keeping them accessible only to authorized users. While not as robust as a fully automated KMS, this offers a practical solution for small-scale projects.</p>
<h2>
<p>  Conclusion: A Balanced Approach is Essential<br />
</p></h2>
<p>Not just using your JWTs, but managing them correctly and securely, is a critical skill in modern application architectures. Based on my own experiences, I can clearly state that both JWT lifecycle management and secret key rotation are cornerstones of robust API security. One mitigates risks associated with token theft, while the other minimizes risks associated with the signing key being leaked.</p>
<p>Implementing these two strategies together makes your system more resilient against attacks from both inside and outside. Always use short-lived <code>access_token</code>s, support them with blacklisting mechanisms when necessary, and don&#8217;t forget to regularly rotate your secret keys. Automating these processes will reduce operational overhead and decrease the likelihood of errors, especially in large-scale and critical systems. Remember, security is not a one-time task but an ongoing process that requires continuous improvement. In my next post, I will discuss [related: event-sourcing implementations in distributed systems].</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/jwt-lifecycle-vs-secret-rotation-which-is-more-secure/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>I built a peer review site for dev portfolios</title>
		<link>https://codango.com/i-built-a-peer-review-site-for-dev-portfolios/</link>
					<comments>https://codango.com/i-built-a-peer-review-site-for-dev-portfolios/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 10:14:45 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/i-built-a-peer-review-site-for-dev-portfolios/</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.amazonaws.com2Fuploads2Farticles2Fqta4ryvr16jyhsvdjh27-gAUu9j-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />Getting feedback on your portfolio is annoying. You post it somewhere, someone says &#8220;looks clean!&#8221; and that&#8217;s the end of it. I wanted something with actual structure, scores from other <a class="more-link" href="https://codango.com/i-built-a-peer-review-site-for-dev-portfolios/">Continue reading <span class="screen-reader-text">  I built a peer review site for dev portfolios</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.amazonaws.com2Fuploads2Farticles2Fqta4ryvr16jyhsvdjh27-gAUu9j-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqta4ryvr16jyhsvdjh27.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqta4ryvr16jyhsvdjh27.png" alt="Homepage" width="800" height="460" /></a> <a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7nue0gdisv2vsbx1n0f.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh7nue0gdisv2vsbx1n0f.png" alt=" " width="800" height="461" /></a><br />
Getting feedback on your portfolio is annoying. You post it somewhere, someone says &#8220;looks clean!&#8221; and that&#8217;s the end of it. I wanted something with actual structure, scores from other devs, with context.</p>
<p>So I made <a href="https://ratemyportfolio.astro.ovh/" rel="noopener noreferrer">RateMyPortfolio</a>. You submit your portfolio URL, people rate it across design, projects, and technical quality. That&#8217;s basically it.</p>
<h2>
<p>  Stack<br />
</p></h2>
<ul>
<li>Flask + SQLAlchemy + PostgreSQL</li>
<li>Tailwind CSS</li>
<li>Raspberry Pi 5 + NVMe, Nginx + Gunicorn</li>
<li>Cloudflare in front</li>
</ul>
<h2>
<p>  Current state<br />
</p></h2>
<p>It works. Two portfolios up, three members. It&#8217;s early.</p>
<p>If you have a portfolio, <a href="https://ratemyportfolio.astro.ovh/" rel="noopener noreferrer">throw it up there</a>. Honest feedback only, no &#8220;looks clean!&#8221;</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/i-built-a-peer-review-site-for-dev-portfolios/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Browser-CLI: Let Your AI Agent Control the Browser from the Command Line</title>
		<link>https://codango.com/browser-cli-let-your-ai-agent-control-the-browser-from-the-command-line/</link>
					<comments>https://codango.com/browser-cli-let-your-ai-agent-control-the-browser-from-the-command-line/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 10:07:18 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/browser-cli-let-your-ai-agent-control-the-browser-from-the-command-line/</guid>

					<description><![CDATA[Ever wanted your AI coding assistant to actually use a browser? Not just read web pages, but click buttons, fill forms, take screenshots, and extract data — all from the <a class="more-link" href="https://codango.com/browser-cli-let-your-ai-agent-control-the-browser-from-the-command-line/">Continue reading <span class="screen-reader-text">  Browser-CLI: Let Your AI Agent Control the Browser from the Command Line</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Ever wanted your AI coding assistant to actually <em>use</em> a browser? Not just read web pages, but click buttons, fill forms, take screenshots, and extract data — all from the terminal?</p>
<p>That&#8217;s exactly why I built <strong>Browser-CLI</strong>.</p>
<h2>
<p>  What is it?<br />
</p></h2>
<p>Browser-CLI is a Go-based command-line tool that wraps Playwright to give AI agents full browser control through simple shell commands. No API keys, no browser extensions, no complex setup — just run a command and you&#8217;re off.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Install</span>
git clone https://github.com/zmysysz/browser-cli
<span class="nb">cd </span>browser-cli <span class="o">&amp;&amp;</span> make build <span class="o">&amp;&amp;</span> make <span class="nb">install
</span>make setup-browsers  <span class="c"># first time only</span>

<span class="c"># Use</span>
browser-cli navigate https://example.com
browser-cli fill <span class="s2">"#search"</span> <span class="s2">"hello world"</span>
browser-cli click <span class="s2">"button[type=submit]"</span>
browser-cli text
</code></pre>
</div>
<h2>
<p>  Why not just use Playwright directly?<br />
</p></h2>
<p>Playwright is great, but it&#8217;s a library — you need to write code to use it. Browser-CLI turns it into a <strong>universal CLI interface</strong> that any AI agent can call without writing a single line of automation code.</p>
<p>This means:</p>
<ul>
<li>
<strong>Claude Code</strong> can browse the web</li>
<li>
<strong>OpenAI Codex</strong> can fill forms and extract data</li>
<li>
<strong>Cursor</strong> can take screenshots and interact with pages</li>
<li>
<strong>Any AI agent</strong> can automate browser tasks through shell commands</li>
</ul>
<h2>
<p>  Key Features<br />
</p></h2>
<ul>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>AI-First Design</strong> — Structured JSON output, auto-managed server, clear command semantics</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f512.png" alt="🔒" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Session Isolation</strong> — Each agent gets its own browser instance via <code>--session</code>
</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f36a.png" alt="🍪" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Cookie Persistence</strong> — Auto save/load, login states preserved across sessions</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f310.png" alt="🌐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Proxy Support</strong> — <code>--proxy http://host:port</code> for restricted networks</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Web Components</strong> — <code>smart-click</code> and <code>pick</code> for custom elements and Shadow DOM</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2328.png" alt="⌨" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Full Keyboard</strong> — Shortcuts, combos, Tab/Enter/Escape, Ctrl+A/C/V</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4c4.png" alt="📄" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>PDF &amp; Screenshot</strong> — Export pages as PDF or PNG</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4c1.png" alt="📁" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>File Upload</strong> — Upload files to any <code>&lt;input type="file"&gt;</code>
</li>
</ul>
<h2>
<p>  30 Commands at a Glance<br />
</p></h2>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Category</th>
<th>Commands</th>
</tr>
</thead>
<tbody>
<tr>
<td>Navigate</td>
<td>
<code>navigate</code>, <code>back</code>, <code>forward</code>, <code>reload</code>
</td>
</tr>
<tr>
<td>Click</td>
<td>
<code>click</code>, <code>click-js</code>, <code>smart-click</code>, <code>right-click</code>, <code>dblclick</code>
</td>
</tr>
<tr>
<td>Input</td>
<td>
<code>fill</code>, <code>type</code>, <code>select</code>, <code>keyboard</code>, <code>upload</code>
</td>
</tr>
<tr>
<td>Extract</td>
<td>
<code>text</code>, <code>screenshot</code>, <code>elements</code>, <code>eval</code>, <code>pdf</code>
</td>
</tr>
<tr>
<td>Utility</td>
<td>
<code>wait</code>, <code>scroll</code>, <code>pick</code>
</td>
</tr>
<tr>
<td>Tabs</td>
<td>
<code>tab-new</code>, <code>tab-list</code>, <code>tab-switch</code>, <code>tab-close</code>
</td>
</tr>
<tr>
<td>Dialogs</td>
<td>
<code>dialog-status</code>, <code>dialog-accept</code>, <code>dialog-dismiss</code>
</td>
</tr>
<tr>
<td>Session</td>
<td>
<code>status</code>, <code>stop</code>, <code>session-list</code>, <code>cookie</code>
</td>
</tr>
</tbody>
</table>
</div>
<h2>
<p>  Integration with AI Tools<br />
</p></h2>
<p>Browser-CLI ships with ready-to-use integration files:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>File</th>
<th>Tool</th>
<th>How to Use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>integrations/claude/browser.md</code></td>
<td>Claude Code</td>
<td>Copy to <code>.claude/commands/</code>
</td>
</tr>
<tr>
<td><code>integrations/codex/browser-cli.md</code></td>
<td>OpenAI Codex</td>
<td>Copy to <code>~/.codex/skills/</code>
</td>
</tr>
<tr>
<td><code>AGENTS.md</code></td>
<td>Cursor, Windsurf</td>
<td>Already in project root</td>
</tr>
<tr>
<td><code>skills/browser-cli/SKILL.md</code></td>
<td>GAL</td>
<td>Copy to <code>~/.gal/skills/</code>
</td>
</tr>
</tbody>
</table>
</div>
<h2>
<p>  Real-World Example<br />
</p></h2>
<p>Here&#8217;s how an AI agent can search GitHub and extract results:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Navigate to GitHub</span>
browser-cli navigate https://github.com/search?q<span class="o">=</span>browser+automation

<span class="c"># Extract search results</span>
browser-cli <span class="nb">eval</span> <span class="s2">"JSON.stringify(
  Array.from(document.querySelectorAll('.repo-list-item a.v-align-middle'))
  .map(a =&gt; ({name: a.textContent.trim(), url: a.href}))
)"</span>

<span class="c"># Take a screenshot</span>
browser-cli screenshot github-results.png
</code></pre>
</div>
<h2>
<p>  Architecture<br />
</p></h2>
<p>Browser-CLI uses a client-server architecture over Unix sockets:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>AI Agent → shell command → browser-cli (client) → Unix socket → server → Playwright → Browser
</code></pre>
</div>
<p>The server auto-starts on first command and stays running. Multiple agents can connect simultaneously with isolated sessions.</p>
<h2>
<p>  No CGO Required<br />
</p></h2>
<p>Pure Go binary, compiles with <code>CGO_ENABLED=0</code>:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Static Linux build</span>
make build-static

<span class="c"># Cross-compile for Windows</span>
<span class="nv">CGO_ENABLED</span><span class="o">=</span>0 <span class="nv">GOOS</span><span class="o">=</span>windows <span class="nv">GOARCH</span><span class="o">=</span>amd64 go build <span class="nt">-o</span> browser-cli.exe <span class="nb">.</span>

<span class="c"># Cross-compile for macOS</span>
<span class="nv">CGO_ENABLED</span><span class="o">=</span>0 <span class="nv">GOOS</span><span class="o">=</span>darwin <span class="nv">GOARCH</span><span class="o">=</span>arm64 go build <span class="nt">-o</span> browser-cli-mac <span class="nb">.</span>
</code></pre>
</div>
<h2>
<p>  Get Started<br />
</p></h2>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code>git clone https://github.com/zmysysz/browser-cli
<span class="nb">cd </span>browser-cli <span class="o">&amp;&amp;</span> make build <span class="o">&amp;&amp;</span> make <span class="nb">install
</span>make setup-browsers
browser-cli navigate https://example.com
</code></pre>
</div>
<p>Star <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2b50.png" alt="⭐" class="wp-smiley" style="height: 1em; max-height: 1em;" /> the repo if you find it useful! Feedback and contributions welcome.</p>
<p><em>This article was drafted with the help of an AI agent — but the tool itself was built by hand. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f609.png" alt="😉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></em></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/browser-cli-let-your-ai-agent-control-the-browser-from-the-command-line/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>I Thought Regex Could Handle It: My Data Extraction Rabbit Hole</title>
		<link>https://codango.com/i-thought-regex-could-handle-it-my-data-extraction-rabbit-hole/</link>
					<comments>https://codango.com/i-thought-regex-could-handle-it-my-data-extraction-rabbit-hole/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 02:00:44 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/i-thought-regex-could-handle-it-my-data-extraction-rabbit-hole/</guid>

					<description><![CDATA[A few months ago, I was building a tool to automatically parse invoice emails. You know the drill: subject line like &#8220;Invoice #12345 from ACME Corp &#8211; $1,234.56 due 2024-03-15&#8221;. <a class="more-link" href="https://codango.com/i-thought-regex-could-handle-it-my-data-extraction-rabbit-hole/">Continue reading <span class="screen-reader-text">  I Thought Regex Could Handle It: My Data Extraction Rabbit Hole</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>A few months ago, I was building a tool to automatically parse invoice emails. You know the drill: subject line like &#8220;Invoice #12345 from ACME Corp &#8211; $1,234.56 due 2024-03-15&#8221;. Seemed straightforward. I spent a day crafting the perfect regex pattern, feeling smug when it worked on the first 10 emails.</p>
<p>Then email #11 arrived. The subject was &#8220;Your invoice from ACME Corp (ref: INV-12345) – please pay $1,234.56 by 2024-03-15&#8221;. My regex broke. I tweaked it. Then email #12 had &#8220;INVOICE: ACME Corp, Amount Due: $1,234.56, Due Date: 2024-03-15&#8221;. My regex grew into a monster with optional groups and lookaheads. I knew I was on the wrong path.</p>
<h2>
<p>  The Dead Ends<br />
</p></h2>
<h3>
<p>  More Regex<br />
</p></h3>
<p>I tried building a library of patterns. It worked for about 60% of cases. Every new vendor introduced a new format. Maintenance was a nightmare. I spent more time debugging regex than building features.</p>
<h3>
<p>  Rule-Based Parsers<br />
</p></h3>
<p>I moved to Python&#8217;s <code>dateutil</code> and some simple string matching. Still fragile. Any slight deviation in date format or wording caused silent failures.</p>
<h3>
<p>  ML with spaCy<br />
</p></h3>
<p>I thought, &#8220;Let&#8217;s train a custom NER model!&#8221; I spent two weeks labeling invoices. The model learned to find monetary amounts and dates, but it couldn&#8217;t understand context—like figuring out which date was the due date vs the invoice date. And retraining for new fields required more data and labeling.</p>
<h2>
<p>  What Eventually Worked: Structured Output with LLMs<br />
</p></h2>
<p>I realized I didn&#8217;t need to <em>understand</em> every format. I needed a system that could read English (or any language) and extract structured data reliably. Large Language Models (LLMs) with <strong>function calling</strong> (or structured output) were the answer.</p>
<p>Here&#8217;s the core technique: instead of asking the model for freeform text, you give it a JSON schema and tell it to output valid JSON matching that schema. This works surprisingly well.</p>
<h3>
<p>  Code Example: Extracting Invoice Data<br />
</p></h3>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">import</span> <span class="n">json</span>
<span class="kn">from</span> <span class="n">openai</span> <span class="kn">import</span> <span class="n">OpenAI</span>

<span class="n">client</span> <span class="o">=</span> <span class="nc">OpenAI</span><span class="p">()</span>

<span class="c1"># Define the output schema as a function definition
</span><span class="n">functions</span> <span class="o">=</span> <span class="p">[</span>
    <span class="p">{</span>
        <span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">extract_invoice</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">description</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Extract invoice details from email body</span><span class="sh">"</span><span class="p">,</span>
        <span class="sh">"</span><span class="s">parameters</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
            <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">object</span><span class="sh">"</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">properties</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
                <span class="sh">"</span><span class="s">vendor_name</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">},</span>
                <span class="sh">"</span><span class="s">invoice_number</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">},</span>
                <span class="sh">"</span><span class="s">amount_due</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">number</span><span class="sh">"</span><span class="p">},</span>
                <span class="sh">"</span><span class="s">due_date</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">format</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">date</span><span class="sh">"</span><span class="p">},</span>
                <span class="sh">"</span><span class="s">currency</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">string</span><span class="sh">"</span><span class="p">}</span>
            <span class="p">},</span>
            <span class="sh">"</span><span class="s">required</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span><span class="sh">"</span><span class="s">vendor_name</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">amount_due</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">due_date</span><span class="sh">"</span><span class="p">]</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">]</span>

<span class="c1"># Example email text (could be from any source)
</span><span class="n">email_text</span> <span class="o">=</span> <span class="sh">"""</span><span class="s">
Subject: Invoice #INV-7890 from Widgets Inc.
Dear customer, your invoice for $567.89 is due by April 30, 2024.
Please pay in USD.
</span><span class="sh">"""</span>

<span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">chat</span><span class="p">.</span><span class="n">completions</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
    <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">gpt-4</span><span class="sh">"</span><span class="p">,</span>
    <span class="n">messages</span><span class="o">=</span><span class="p">[</span>
        <span class="p">{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">system</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Extract the requested fields from the email. Return valid JSON.</span><span class="sh">"</span><span class="p">},</span>
        <span class="p">{</span><span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="n">email_text</span><span class="p">}</span>
    <span class="p">],</span>
    <span class="n">functions</span><span class="o">=</span><span class="n">functions</span><span class="p">,</span>
    <span class="n">function_call</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">name</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">extract_invoice</span><span class="sh">"</span><span class="p">}</span>
<span class="p">)</span>

<span class="c1"># Parse the structured output
</span><span class="n">extracted</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="nf">loads</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">choices</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">message</span><span class="p">.</span><span class="n">function_call</span><span class="p">.</span><span class="n">arguments</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">extracted</span><span class="p">)</span>
</code></pre>
</div>
<p>Output:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight json"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"vendor_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Widgets Inc."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"invoice_number"</span><span class="p">:</span><span class="w"> </span><span class="s2">"INV-7890"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"amount_due"</span><span class="p">:</span><span class="w"> </span><span class="mf">567.89</span><span class="p">,</span><span class="w">
  </span><span class="nl">"due_date"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2024-04-30"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"currency"</span><span class="p">:</span><span class="w"> </span><span class="s2">"USD"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre>
</div>
<h3>
<p>  Why This Works<br />
</p></h3>
<ul>
<li>The model uses its language understanding to infer fields from context.</li>
<li>You control the schema, so output is predictable.</li>
<li>It works with minimal prompt engineering—just describe what you want.</li>
<li>Handles variations: &#8220;due by April 30, 2024&#8221;, &#8220;due date: 2024-04-30&#8221;, &#8220;payment deadline: 30/04/2024&#8221; all produce the same ISO date.</li>
</ul>
<h2>
<p>  The Hard Lessons<br />
</p></h2>
<p>LLMs aren&#8217;t magic. Here&#8217;s what I learned:</p>
<ol>
<li>
<strong>Cost</strong> – Each extraction costs pennies. For small volumes (hundreds a day) it&#8217;s fine. For millions, you need a cheaper alternative.</li>
<li>
<strong>Latency</strong> – OpenAI&#8217;s response time is usually 1-3 seconds. For real-time apps, that might be too slow.</li>
<li>
<strong>Hallucinations</strong> – If the email doesn&#8217;t contain a required field, the model might make one up. You need to validate outputs and set <code>required</code> fields wisely.</li>
<li>
<strong>Context length</strong> – Long emails might get truncated. Chunking and a two-stage pipeline (classify + extract) helps.</li>
<li>
<strong>Model choice</strong> – GPT-4 is best, but GPT-3.5-turbo sometimes fails on complex schemas. For production, I switched to a dedicated API that handles retries and validation under the hood—there are several out there, including services like <a href="https://ai.interwestinfo.com/" rel="noopener noreferrer">Interwest Info&#8217;s AI API</a> (I used it after hitting rate limits with OpenAI). But the technique remains the same.</li>
</ol>
<h2>
<p>  When NOT to Use This Approach<br />
</p></h2>
<ul>
<li>If your data is highly structured and fixed (e.g., CSV columns), regex or a parser is faster and cheaper.</li>
<li>If you need real-time extraction (milliseconds), LLMs are too slow.</li>
<li>If you need guaranteed correctness (e.g., medical data), LLMs can&#8217;t provide that.</li>
</ul>
<h2>
<p>  What I&#8217;d Do Differently Next Time<br />
</p></h2>
<p>I&#8217;d start with the LLM approach from the beginning, but I&#8217;d also build a fallback chain:</p>
<ol>
<li>Try regex for known patterns.</li>
<li>If that fails, call an LLM.</li>
<li>Log everything to improve the regex library over time.</li>
</ol>
<p>Also, I&#8217;d use a structured output library like <code>jsonformer</code> or <code>outlines</code> to constrain generation even more.</p>
<h2>
<p>  The Takeaway<br />
</p></h2>
<p>Regex is great for well-defined problems. But real-world text is messy. LLMs give us a way to handle that mess without building a million rules. The key is to treat them as a tool in your parsing toolbox—not a silver bullet.</p>
<p>Now I&#8217;m curious: <strong>What&#8217;s your go-to approach for extracting data from messy documents? Still wrestling with regex, or have you joined the LLM camp? Let me know in the comments.</strong></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/i-thought-regex-could-handle-it-my-data-extraction-rabbit-hole/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Zero Trust Isn&#8217;t Just for Enterprises: What Developers Need to Know About Sharing Files in 2026</title>
		<link>https://codango.com/zero-trust-isnt-just-for-enterprises-what-developers-need-to-know-about-sharing-files-in-2026/</link>
					<comments>https://codango.com/zero-trust-isnt-just-for-enterprises-what-developers-need-to-know-about-sharing-files-in-2026/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 02:00:39 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/zero-trust-isnt-just-for-enterprises-what-developers-need-to-know-about-sharing-files-in-2026/</guid>

					<description><![CDATA[Hey folks! &#x1f44b; As developers, we’re constantly sharing files—configuration snippets, build artifacts, design mockups, error logs, and quick code samples. For a long time, our collective file-sharing habits leaned heavily <a class="more-link" href="https://codango.com/zero-trust-isnt-just-for-enterprises-what-developers-need-to-know-about-sharing-files-in-2026/">Continue reading <span class="screen-reader-text">  Zero Trust Isn&#8217;t Just for Enterprises: What Developers Need to Know About Sharing Files in 2026</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Hey folks! <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f44b.png" alt="👋" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>As developers, we’re constantly sharing files—configuration snippets, build artifacts, design mockups, error logs, and quick code samples. For a long time, our collective file-sharing habits leaned heavily toward convenience, often at the expense of robust security. </p>
<p>But as we navigate 2026, the security landscape has fundamentally shifted. <strong>&#8220;Zero Trust&#8221;</strong> is no longer just a buzzword tossed around by enterprise network architects and SecOps teams; it is a critical mindset that we, as individual developers, must bake into our daily workflows.</p>
<p>The reality check is brutal: a single open-ended sharing link containing a sensitive <code>.env</code> file or a core source code snippet can become an entry point for a major data breach, leading to compromised credentials or intellectual property leaks. The traditional &#8220;perimeter security&#8221; model—assuming everything inside a network or an authorized chat app is safe—is dead. Zero Trust operates on a simple, uncompromising principle: <strong>&#8220;Never trust, always verify.&#8221;</strong></p>
<p>Let’s look at how we can implement a Zero Trust mindset in our everyday developer workflows without crippling our productivity.</p>
<h2>
<p>  1. Embrace Ephemeral Sharing and &#8216;Just-in-Time&#8217; Access<br />
</p></h2>
<p>One of the biggest security vulnerabilities in a developer’s workflow is persistent, unrestricted access. Think about how many cloud storage links or public pastebins you’ve generated that are still live right now, waiting for anyone (or any web scraper) to stumble upon them.</p>
<p>Moving toward a Zero Trust model means treating file sharing as a temporary state. We need to embrace ephemeral sharing and <strong>Just-in-Time (JIT)</strong> access:</p>
<ul>
<li>  <strong>Time-Bound Links:</strong> Share files using links that automatically expire after a set duration (e.g., 5 minutes, 1 hour, or 1 day). This drastically reduces the window of exposure.</li>
<li>  <strong>Single-Use Access (Burn-after-reading):</strong> For highly sensitive payloads like database dumps or temporary credentials, use mechanisms that completely delete the file from the server immediately after the first download.</li>
<li>  <strong>Recipient Verification:</strong> Whenever possible, enforce a quick second layer of verification—such as a temporary passcode or basic email verification—before granting access.</li>
</ul>
<p>When I was building <strong><a href="https://www.simpledrop.net/" rel="noopener noreferrer">SimpleDrop</a></strong>, this exact philosophy was my core guiding principle. I wanted a tool that made sharing blazing fast but inherently secure. By making links strictly temporary and letting them expire gracefully, you eliminate the risk of forgotten, dangling assets. It proves that limiting exposure by limiting duration doesn&#8217;t have to be a chore—it can be secure by design.</p>
<h2>
<p>  2. Prioritize Client-Side Encryption and Data Integrity<br />
</p></h2>
<p>Relying solely on transport-layer encryption (like standard HTTPS) is a half-measure in a true Zero Trust model. What if the storage server itself gets compromised? What if a malicious actor orchestrates a Man-in-the-Middle (MitM) attack before the data hits the cloud provider&#8217;s encryption engine?</p>
<p>To achieve true Zero Trust, <strong>the encryption and validation processes should ideally start on the client side (your local machine) before the data ever touches the wire.</strong></p>
<ul>
<li>  <strong>End-to-End Encryption (E2EE):</strong> Utilize tools where only you (the sender) and the intended recipient hold the cryptographic keys required to read the data. The hosting server should only ever see encrypted garbage.</li>
<li>  <strong>Cryptographic Hashing for Integrity:</strong> To ensure your files haven&#8217;t been tampered with or corrupted in transit, generate a cryptographic hash (like SHA-256) of the file locally. You can share this hash via an out-of-band channel so the recipient can verify the download&#8217;s integrity instantly.</li>
</ul>
<p>Here’s a quick, lightweight JavaScript snippet demonstrating how you can hash file contents directly in the browser before shipping them anywhere:</p>
<div class="highlight js-code-highlight">
<pre class="highlight plaintext"><code>
javascript
async function hashFileContent(file) {
  const buffer = await file.arrayBuffer();
  const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  const hexHash = hashArray.map(b =&gt; b.toString(16).padStart(2, '0')).join('');
  return hexHash;
}

// Example usage in an upload event handler:
// const file = event.target.files[0];
// const hash = await hashFileContent(file);
// console.log('File SHA-256 Hash:', hash);
</code></pre>
</div>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/zero-trust-isnt-just-for-enterprises-what-developers-need-to-know-about-sharing-files-in-2026/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>My First Embedded System with a 40-Year-Old Machine</title>
		<link>https://codango.com/my-first-embedded-system-with-a-40-year-old-machine/</link>
					<comments>https://codango.com/my-first-embedded-system-with-a-40-year-old-machine/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 02:00:01 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/my-first-embedded-system-with-a-40-year-old-machine/</guid>

					<description><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.amazonaws.com2Fuploads2Farticles2Fiezbxyh6884vestb14ps-91hVHe-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" />What happened? Recently, I got an unusual task: bring an old medical machine back to life. The machine had been built more than 40 years ago (older than me &#x1f643;) <a class="more-link" href="https://codango.com/my-first-embedded-system-with-a-40-year-old-machine/">Continue reading <span class="screen-reader-text">  My First Embedded System with a 40-Year-Old Machine</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<img width="150" height="150" src="https://codango.com/wp-content/uploads/https3A2F2Fdev-to-uploads.s3.amazonaws.com2Fuploads2Farticles2Fiezbxyh6884vestb14ps-91hVHe-150x150.webp" class="attachment-thumbnail size-thumbnail wp-post-image" alt="" decoding="async" loading="lazy" /><h2>
<p>  What happened?<br />
</p></h2>
<p>Recently, I got an unusual task: bring an old <a href="https://gccpathology.com/comprehensive-technical-overview-of-tissue-processors-in-histopathology-laboratories/" rel="noopener noreferrer">medical machine</a> back to life.</p>
<p>The machine had been built more than 40 years ago (older than me <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f643.png" alt="🙃" class="wp-smiley" style="height: 1em; max-height: 1em;" />) and its original control mechanism was completely mechanical. Unfortunately, that mechanism had been seriously damaged, and replacing it wasn&#8217;t really an option.</p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiezbxyh6884vestb14ps.jpeg" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiezbxyh6884vestb14ps.jpeg" alt="Histokinette E7326" width="800" height="600" /></a></p>
<p>So the solution was to redesign the controller from scratch using an Arduino.</p>
<p>That’s where my embedded systems journey started&#8230;</p>
<h2>
<p>  The task&#8230;<br />
</p></h2>
<p>The device itself was pretty unusual.</p>
<p>Its job was to move tissue samples through a sequence of chemical tanks, where each tank contained a different substance used during processing.</p>
<p>Our machine had 12 tanks in total.</p>
<p>The first 10 tanks required the tissue to remain submerged for one hour each. The last two tanks were different: they contained paraffin wax, so the samples had to stay there for two hours.</p>
<p>Those final stages were especially sensitive because the paraffin had to be completely melted before the tissue could enter the tank. Otherwise, the sample could be damaged.</p>
<p>The original machine already had:</p>
<ul>
<li>wax temperature sensors for the paraffin tanks</li>
<li>two heating elements</li>
<li>vibration motor that used to shake the samples while they were inside the tanks.</li>
<li>a bottom sensor to detect when the sample holder was fully submerged</li>
<li>a top sensor to detect when the mechanism was preparing to move the samples to the next tank</li>
<li>a motor controlling the movement mechanism</li>
</ul>
<p>The movement itself was surprisingly primitive.</p>
<p>The main motor only knew how to perform a fixed sequence:<br />
<strong>down → up → move → down → up&#8230;</strong></p>
<p>There was no position control, no direction switching, and no speed adjustment. We could only turn the motor on or off and trust the mechanical system to complete the cycle correctly.</p>
<p>At first glance, it sounded simple.</p>
<p>Then came the real problems:</p>
<ul>
<li>handling power outages</li>
<li>detecting damaged sensors</li>
<li>recovering from interrupted cycles</li>
<li>allowing operators to skip tanks safely</li>
<li>preventing samples from being destroyed by overheated or unmelted paraffin</li>
</ul>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipyudmsdkikoopeyasrr.jpg" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fipyudmsdkikoopeyasrr.jpg" alt="source: Wikipedia" width="390" height="255" /></a> <br />
<em>And this wasn’t some hobby project.</em></p>
<p>It was a medical device.</p>
<p>If my code crashed, a patient could lose a tissue sample that had already gone through an <strong>entire week</strong> of processing across multiple machines <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f910.png" alt="🤐" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h2>
<p>  The Hardest Part&#8230; Defining the Task!<br />
</p></h2>
<p>When I was less experienced, I often heard that programming is only a small percentage of a software developer&#8217;s job, and that the biggest time sink is defining the actual task with stakeholders.</p>
<p>But why?</p>
<p>Because clients themselves often don&#8217;t fully realize what they want. They say things like:</p>
<blockquote>
<p>&#8220;Make it work on time, without bugs, and make it easy to maintain!&#8221;</p>
</blockquote>
<p>Okay, but sometimes the client is asking for the wrong thing from an engineering perspective, and sometimes we, as developers, overcomplicate things and forget about the user experience.</p>
<p>In this project, I was part of a team of three engineers. I was the software developer, the second was an electrical engineer (I spent most of my time discussing technical nuances with him), and the third was a mechanical engineer.</p>
<p>Oh, how many times I heard:</p>
<blockquote>
<p>&#8220;Keep it simple. Just remove that. It&#8217;s easy. Ignore that sensor&#8230;&#8221;</p>
</blockquote>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0qb8b9g462usimtmsnql.jpg" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0qb8b9g462usimtmsnql.jpg" alt="It_IS_EASY" width="800" height="812" /></a></p>
<p>Sometimes I defended those decisions, and sometimes I simply noted the possible consequences of a particular implementation.</p>
<p>After many meetings and countless hours spent searching the internet and asking LLMs for reliable descriptions of how these machines actually worked, the technical requirements were finally ready&#8230; <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f60a.png" alt="😊" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f62f.png" alt="😯" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h2>
<p>  Implementation&#8230; First Round 🙂<br />
</p></h2>
<p>Here we go. I implemented the code using an FSM (Finite State Machine), and everything seemed to be working well.</p>
<p>Then I heard the electrical engineer say:</p>
<blockquote>
<p>&#8220;Okay, we think it should handle power outages too&#8230;&#8221;</p>
</blockquote>
<p>If you could have seen my face ):</p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9zf8d2834guepuvghrcr.jpg" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9zf8d2834guepuvghrcr.jpg" alt="Crying_Boy" width="800" height="461" /></a></p>
<p>The funny part was that I had asked several times about handling power loss, and the answer was always:</p>
<blockquote>
<p>&#8220;We don’t need that. The power supply is backed by a UPS, so we don’t expect any interruptions.&#8221;</p>
</blockquote>
<p>Even more interesting, they were against adding EEPROM, which I considered necessary because I wanted to store the current position and state.</p>
<p>They said:</p>
<blockquote>
<p>&#8220;No, no, that will make everything too complicated.&#8221;</p>
</blockquote>
<p>After a long discussion, I finally said:</p>
<blockquote>
<p>&#8220;Okay, but I&#8217;ll list the drawbacks of this solution. We&#8217;ll rely heavily on sensors, and the total downtime of the tank liquid process will be unknown after a restart.&#8221;</p>
</blockquote>
<p>And the response was:</p>
<blockquote>
<p>&#8220;No, no, everything will be fine.&#8221;</p>
</blockquote>
<h2>
<p>  Rewriting the Code&#8230;<br />
</p></h2>
<p>Did I really rewrite everything?</p>
<p>No.</p>
<p>Because I implemented the system as an FSM (Finite State Machine). In simple terms, every state can only transition to specific other states and cannot randomly jump between them.</p>
<p>It&#8217;s a bit like real life.</p>
<p>You can&#8217;t be asleep and awake at the same time. <br />
You can&#8217;t be running a marathon while sitting perfectly still.</p>
<p>These are mutually exclusive states. If you somehow find yourself doing both, you’re either dreaming or you probably need to see a doctor. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f60a.png" alt="😊" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>In our case, if the machine ends up in an impossible state, the customer should contact us or call a technician.<br />
And that&#8217;s why I didn&#8217;t have to rewrite everything.</p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuy5x9mg1ss9ylc9z7u2.png" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvuy5x9mg1ss9ylc9z7u2.png" alt="Code_Isolation" width="640" height="480" /></a></p>
<p>Because the machine was built around isolated states, I could simply add a recovery mode. If the sample holder was down, everything was normal and the machine continued as usual. If it was up or somewhere in between, the software tried to figure out its position and recover safely from there.</p>
<p>One more point for FSMs. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f604.png" alt="😄" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h2>
<p>  Example of My Mess&#8230;)<br />
</p></h2>
<p>Let&#8217;s look at one of the methods responsible for moving the samples to the next tank:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight java"><code><span class="c1">// Finite State Machine Transition Configuration</span>
<span class="o">{</span>
    <span class="n">verifyingPredicate</span><span class="o">,</span>              <span class="c1">// Condition check</span>
    <span class="no">S_IDLE</span><span class="o">,</span>                          <span class="c1">// Next state if false</span>
    <span class="no">S_UNKNOWN_DIRECTION_RECOVERY</span><span class="o">,</span>    <span class="c1">// Next state if true</span>
    <span class="n">verifyingProcess</span><span class="o">,</span>                <span class="c1">// State body logic</span>
    <span class="n">verifyingActionChanged</span><span class="o">,</span>          <span class="c1">// UI/Side-effects hook</span>
    <span class="no">VERIFICATION_DELAY_MS</span><span class="o">,</span>           <span class="c1">// State stabilization timeout</span>
    <span class="no">PREDIC_TIMER</span>                     <span class="c1">// Timer configuration</span>
<span class="o">},</span>
</code></pre>
</div>
<p>First, it’s an array of transitions, so each transition includes the previous one. <em>Don’t ask me how I ended up with this architecture—it actually comes from the</em> <a href="https://github.com/MicroBeaut/Finite-State" rel="noopener noreferrer">Finite State library</a>.</p>
<p>The <code>predicate</code> runs every cycle of the main loop. It decides where we go next: if it returns <code>true</code>, we move to <code>S_UNKNOWN</code>, otherwise we go back to <code>S_IDLE</code>.</p>
<p><code>verifyingProcess</code> runs after the predicate decision. It’s basically the body of the state — all the actual work we want to do while we&#8217;re in it. Nothing fancy, just the state logic itself.</p>
<p>Then we have <code>verifyingActionChanged</code>. This one is triggered when we enter or exit the state, so it’s perfect for side effects like UI updates. In this case, we only print a message on the LCD when we enter the state.</p>
<p><code>VERIFICATION_DELAY_MS</code> is just a timing parameter (around 10 seconds here). And <code>PREDIC_TIMER</code> tells the system to temporarily ignore the predicate for that period — so we don’t switch states immediately. That delay gives the system time to check the tank ID and stabilize all sensors before making a decision.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight cpp"><code><span class="kt">bool</span> <span class="nf">verifyingPredicate</span><span class="p">(</span><span class="n">id_t</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">bottomLimit</span><span class="p">.</span><span class="n">isActive</span><span class="p">())</span>
        <span class="k">return</span> <span class="nb">false</span><span class="p">;</span>
    <span class="k">return</span> <span class="nb">true</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">void</span> <span class="nf">verifyingProcess</span><span class="p">(</span><span class="n">id_t</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">syncTankID</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span>
<span class="p">}</span>

<span class="kt">void</span> <span class="nf">verifyingActionChanged</span><span class="p">(</span><span class="n">EventArgs</span> <span class="n">e</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">e</span><span class="p">.</span><span class="n">action</span> <span class="o">==</span> <span class="n">ENTRY</span><span class="p">)</span>
        <span class="n">lcdShowStatus</span><span class="p">(</span><span class="n">F</span><span class="p">(</span><span class="s">"Initializing"</span><span class="p">),</span> <span class="n">F</span><span class="p">(</span><span class="s">"Wait 10 seconds"</span><span class="p">));</span>
<span class="p">}</span>
</code></pre>
</div>
<p>At first glance, it looks pretty simple, but there’s a lot going on here.</p>
<p>The predicate is basically the decision point: if the bottom limit switch is active, we assume the sample holder is in the correct position and continue normal flow. If not, we drop into recovery mode and try to figure out what actually happened.</p>
<p>The process step syncs the internal tank ID, making sure the software state matches the physical position of the machine.</p>
<p>And finally, verifyingActionChanged() is just a small UI hook — when we enter this state, we show a status message on the LCD so the operator knows the system is doing a verification step.</p>
<p>It doesn’t look like much, but this is basically how the whole system is built: small states, each doing one thing, and a lot of safety logic hidden in simple checks like this.</p>
<h1>
<p>  Final Implementation&#8230; Or Not So Final?<br />
</p></h1>
<p>I still remember testing that code on the real machine.</p>
<p>Fortunately, I had implemented two modes:</p>
<ul>
<li>
<strong>Normal mode</strong>, where the machine completed its cycle in about 14 hours.</li>
<li>
<strong>Test mode</strong>, where the same cycle took only 3 minutes.</li>
</ul>
<p>That allowed us to run many complete tests within a single hour.</p>
<p>And that&#8217;s when we encountered electrical noise.</p>
<p>The processor would stop, restart, and occasionally get stuck in a loop.</p>
<p>Luckily, I had already implemented a watchdog timer (basically, if the software gets stuck for more than two seconds, it automatically restarts).</p>
<p>The electrical engineer said he fixed the noise issue, so&#8230; we&#8217;ll see. 🙂</p>
<p><a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq8eah5alqg3xkz336muo.jpg" class="article-body-image-wrapper"><img loading="lazy" decoding="async" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq8eah5alqg3xkz336muo.jpg" alt="Chiling_Cat" width="236" height="354" /></a></p>
<p>We spent a huge amount of time testing different situations and edge cases:</p>
<ul>
<li>Processor timeouts</li>
<li>Thermostats</li>
<li>Sensor failures</li>
<li>Tank identification</li>
<li>Recovery scenarios</li>
</ul>
<p>As with most embedded development, and especially in medical-related systems, testing often takes more time than development itself.</p>
<p>The real world is much less predictable than the development environment.</p>
<p>So, in the end, I can say that I really enjoyed working on this project with the team. If I sounded a little annoyed in some parts of the article, that&#8217;s just because I wanted to share the reality of the development process. <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f604.png" alt="😄" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>As a small gift for reading this article, you can check out the GitHub repository with the full source code and all commits.<br />
Bye! 🙂</p>
<p><a href="https://github.com/abstract-333/Tissue-Processor" rel="noopener noreferrer">Source Code ^_^</a></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/my-first-embedded-system-with-a-40-year-old-machine/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SNMP or NetFlow in Network Monitoring: Why Does the Choice Remain</title>
		<link>https://codango.com/snmp-or-netflow-in-network-monitoring-why-does-the-choice-remain/</link>
					<comments>https://codango.com/snmp-or-netflow-in-network-monitoring-why-does-the-choice-remain/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 01:52:43 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/snmp-or-netflow-in-network-monitoring-why-does-the-choice-remain/</guid>

					<description><![CDATA[Network monitoring is one of the most fundamental responsibilities of a system administrator or network engineer. When traffic slows down, an application becomes unresponsive, or there&#8217;s a suspicion of a <a class="more-link" href="https://codango.com/snmp-or-netflow-in-network-monitoring-why-does-the-choice-remain/">Continue reading <span class="screen-reader-text">  SNMP or NetFlow in Network Monitoring: Why Does the Choice Remain</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Network monitoring is one of the most fundamental responsibilities of a system administrator or network engineer. When traffic slows down, an application becomes unresponsive, or there&#8217;s a suspicion of a security vulnerability, one of the first places we look is the network layer. At this point, we have two powerful tools, but with different philosophies: SNMP and NetFlow. The question of which is better is a debate I&#8217;ve heard in the industry for twenty years, and there&#8217;s still no clear &#8220;this is better&#8221; answer. In my experience, using these two technologies as complementary rather than interchangeable often provides the most accurate solution.</p>
<p>While searching for the cause of delayed shipment reports in an ERP system at a manufacturing company, I first looked at server resources, then database queries&#8230; But the real problem turned out to be on the network side, with slowdowns in communication between different VLANs. In such scenarios, having the right monitoring data is critical for resolving the issue at its root. So, SNMP or NetFlow, where and how should they be used? In this post, I will delve into this dilemma based on my own experiences.</p>
<blockquote>
<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2139.png" alt="ℹ" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Network Monitoring Practice</strong></p>
<p>Network monitoring not only detects performance issues but is also indispensable for security auditing, capacity planning, and business continuity. For me, it&#8217;s like a surgeon&#8217;s patient monitor; it allows me to make correct decisions with real-time data.</p>
</blockquote>
<h2>
<p>  SNMP: The Power and Limitations of the Traditional Observer<br />
</p></h2>
<p>SNMP (Simple Network Management Protocol), as its name suggests, is a simple protocol designed to manage and monitor network devices. It has been around since the 90s and is still used as the default monitoring method on many devices. It primarily works by querying data structures called Management Information Bases (MIBs) on devices. These MIBs contain a wealth of information, such as device CPU usage, memory status, disk space, network interface status, and incoming/outgoing traffic counters.</p>
<p>For me, SNMP is very useful for quickly checking a device&#8217;s general health. For example, I can see in seconds whether a switch port is physically up or down, or what percentage of a server&#8217;s CPU is being utilized, using SNMP. Especially the SNMPv2c version can be quickly deployed due to its simple configuration. However, this simplicity also brings some limitations.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Example of querying interface information of a device with SNMPv2c</span>
<span class="c"># 'public' community string and 192.168.1.1 IP address are assumed.</span>
<span class="c"># ifDescr lists interface descriptions.</span>
snmpwalk <span class="nt">-v</span> 2c <span class="nt">-c</span> public 192.168.1.1 .1.3.6.1.2.1.2.2.1.2

<span class="c"># Example output:</span>
<span class="c"># IF-MIB::ifDescr.1 = STRING: "lo"</span>
<span class="c"># IF-MIB::ifDescr.2 = STRING: "eth0"</span>
<span class="c"># IF-MIB::ifDescr.3 = STRING: "wlan0"</span>
</code></pre>
</div>
<p>The biggest disadvantage of SNMP is that it collects data based on &#8220;polling.&#8221; That is, the monitoring server queries devices at regular intervals (usually 1-5 minutes). This means lower granularity. If a sudden traffic surge or a brief outage occurs, we might miss it if it doesn&#8217;t coincide with a polling interval. This happened on an internal banking platform; due to a 10-minute polling interval, we only noticed brief but critical network congestions after user complaints. Furthermore, sending community strings unencrypted in v1 and v2c versions poses a serious security vulnerability. While SNMPv3 addresses these issues, its configuration becomes much more complex and may not be supported on all devices.</p>
<blockquote>
<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> SNMP Security Risk</strong></p>
<p>When using SNMPv1 and v2c, it&#8217;s important to remember that <code>community string</code>s are transmitted in plain text. This means an attacker listening to network traffic can gather information about your device and, in some cases, even change its configuration. In production environments, it&#8217;s essential to switch to SNMPv3 if possible, or at least restrict access with ACLs.</p>
</blockquote>
<h2>
<p>  NetFlow (and IPFIX): In-depth Traffic Analysis<br />
</p></h2>
<p>NetFlow, developed by Cisco and later generalized by IETF standard IPFIX (IP Flow Information Export), is a protocol that analyzes network traffic on a &#8220;flow&#8221; basis. Unlike SNMP, NetFlow works by the device itself &#8220;exporting&#8221; a summary of traffic (such as source/destination IP, ports, protocol, byte/packet counts) to a collector. Since this is a push-based model rather than polling, it provides much more detailed and real-time traffic visibility.</p>
<p>For me, NetFlow is like reading the DNA of the network. I can instantly see which user is using which application how much, which server is sending how much traffic externally, and even the source and destination of a DDoS attack. On one occasion, when there was an unexpected traffic surge in the backend of my own side project, I was able to quickly identify which IPs were sending abnormal requests thanks to NetFlow logs. This level of detail goes far beyond SNMP&#8217;s information of &#8220;how much traffic passed through the port.&#8221;
</p>
<div class="highlight js-code-highlight">
<pre class="highlight json"><code><span class="err">//</span><span class="w"> </span><span class="err">Example</span><span class="w"> </span><span class="err">of</span><span class="w"> </span><span class="err">a</span><span class="w"> </span><span class="err">simplified</span><span class="w"> </span><span class="err">NetFlow</span><span class="w"> </span><span class="err">/</span><span class="w"> </span><span class="err">IPFIX</span><span class="w"> </span><span class="err">record</span><span class="w">
</span><span class="p">{</span><span class="w">
  </span><span class="nl">"source_ip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.1.10"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"destination_ip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"10.0.0.5"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"source_port"</span><span class="p">:</span><span class="w"> </span><span class="mi">54321</span><span class="p">,</span><span class="w">
  </span><span class="nl">"destination_port"</span><span class="p">:</span><span class="w"> </span><span class="mi">80</span><span class="p">,</span><span class="w">
  </span><span class="nl">"protocol"</span><span class="p">:</span><span class="w"> </span><span class="mi">6</span><span class="p">,</span><span class="w"> </span><span class="err">//</span><span class="w"> </span><span class="err">TCP</span><span class="w">
  </span><span class="nl">"bytes_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">123456</span><span class="p">,</span><span class="w">
  </span><span class="nl">"packets_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">120</span><span class="p">,</span><span class="w">
  </span><span class="nl">"start_time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-06-02T10:00:00Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-06-02T10:00:15Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"interface_in"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="w">
  </span><span class="nl">"interface_out"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre>
</div>
<p>NetFlow&#8217;s power is undeniable, but it also has its limitations. First, not every network device supports NetFlow export. Especially on older or low-cost switches, this feature might be hard to find. Second, collecting and analyzing NetFlow data requires a robust collector infrastructure. The volume of incoming data can be very large; processing thousands of flow records per second demands high disk I/O and CPU requirements. Third, NetFlow does not directly provide system-level metrics like device CPU or memory usage; it only provides traffic-related information. I remember once losing critical traffic data at a large Turkish e-commerce site because the NetFlow collector&#8217;s disk I/O was insufficient. I addressed a similar situation in my post [related: Disk I/O performance issues and solutions].</p>
<h2>
<p>  Key Differences and My Perspective on Trade-offs<br />
</p></h2>
<p>The fundamental difference between SNMP and NetFlow lies in the type of data they collect and their collection methods. SNMP provides information about a device&#8217;s &#8220;health status&#8221; and &#8220;performance metrics&#8221; (CPU, RAM, port status, total traffic counters), while NetFlow provides detailed traffic flow information about &#8220;who is talking to whom.&#8221; This distinction is the most critical point to consider when making a choice.</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Feature</th>
<th>SNMP</th>
<th>NetFlow (IPFIX)</th>
<th>My Commentary</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Data Type</strong></td>
<td>Device metrics (CPU, RAM, disk), port status, total traffic counters</td>
<td>IP flow information (source/destination IP, port, protocol, bytes/packets)</td>
<td>One tells &#8220;device status,&#8221; the other &#8220;what&#8217;s happening on the network.&#8221; Both are needed.</td>
</tr>
<tr>
<td><strong>Collection Method</strong></td>
<td>Polling (monitoring server queries)</td>
<td>Export (device sends to collector)</td>
<td>Polling is delayed, export is real-time. Export is better for quick problem detection.</td>
</tr>
<tr>
<td><strong>Granularity</strong></td>
<td>Low (depends on polling interval)</td>
<td>High (on flow termination or timeout)</td>
<td>NetFlow for detailed analysis, SNMP for general status.</td>
</tr>
<tr>
<td><strong>Overhead</strong></td>
<td>Load on network and device depending on polling frequency</td>
<td>High load on device CPU and collector</td>
<td>Excessive use of either causes problems. Balancing is important.</td>
</tr>
<tr>
<td><strong>Security</strong></td>
<td>v1/v2c vulnerable, v3 secure</td>
<td>Data can be sensitive, collector security is critical</td>
<td>Security configuration should not be neglected for either.</td>
</tr>
<tr>
<td><strong>Use Case</strong></td>
<td>General device health, capacity planning</td>
<td>Anomaly detection, DDoS mitigation, QoS verification, application traffic visibility</td>
<td>Different tools for different problems.</td>
</tr>
<tr>
<td><strong>Hardware Support</strong></td>
<td>Broad (almost all network devices)</td>
<td>Requires specific hardware/software support</td>
<td>Budget and existing infrastructure are decisive here.</td>
</tr>
</tbody>
</table>
</div>
<p>This table is essentially a trade-off matrix. If I&#8217;m only interested in whether a device is alive and how much traffic is passing through its ports, SNMP is a much simpler and sufficient solution. But if I want to know who is talking to whom on my network, which application is consuming bandwidth, or the details of a potential attack, I have no choice but NetFlow. Once, in a customer project, I didn&#8217;t deploy any rules for VLAN segmentation without monitoring all traffic flow with NetFlow. This allowed me to see which traffic a wrong rule was cutting off in seconds. Otherwise, I would have been bogged down with complaints like &#8220;I can ping, but my application isn&#8217;t working.&#8221;</p>
<h2>
<p>  Real-World Scenarios and My Experiences<br />
</p></h2>
<p>Based on my experiences, I want to give a few examples of how I&#8217;ve used these two protocols in different scenarios. These examples will better explain why the choice remains complex.</p>
<p><strong>Scenario 1: A Simple Office Network and SNMP</strong></p>
<p>When managing the network infrastructure of an SME, budget constraints and simplicity were priorities. I had a few managed switches and a firewall. Monitoring the port statuses, uplink speeds, and basic CPU/memory usage of these devices was sufficient for me. I easily handled this with SNMPv2c. I collected basic metrics by polling every 5 minutes and set up alarms with a simple monitoring system like Nagios.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Get total bytes passed through eth0 interface (OID may vary)</span>
<span class="c"># This value continuously increases, and we calculate traffic speed by taking the difference.</span>
snmpget <span class="nt">-v</span> 2c <span class="nt">-c</span> public 192.168.1.1 IF-MIB::ifInOctets.2

<span class="c"># Example output:</span>
<span class="c"># IF-MIB::ifInOctets.2 = Counter32: 1234567890</span>
</code></pre>
</div>
<p>This approach provided quick answers to questions like &#8220;is a port down?&#8221; or &#8220;is there an anomaly on the internet line?&#8221;. However, when more specific questions arose, such as &#8220;why is user X slowing down the internet?&#8221;, I found SNMP to be insufficient. Just seeing the total traffic wasn&#8217;t enough for a detailed analysis. This was a typical example showing that SNMP is good for general health checks but weak for in-depth traffic analysis.</p>
<p><strong>Scenario 2: Large-Scale Production Environment and NetFlow</strong></p>
<p>While developing an ERP for a manufacturing company, the dependency of critical operator screens and production line integrations (like iSCSI supply chain) on network performance was very high. Here, not just the general health of devices, but how much bandwidth a specific production line&#8217;s communication with a specific server was using, or if there was any anomaly at a given moment, was vital. At that time, together with the network team, we enabled NetFlow export on the main routers and switches.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight shell"><code><span class="c"># Example of querying traffic from a specific source IP from data collected</span>
<span class="c"># with nfdump or a similar tool for a NetFlow collector on Linux.</span>
<span class="c"># This is a logical query example, not a command.</span>
nfdump <span class="nt">-r</span> /data/nfcapd.202606021000 <span class="nt">-A</span> srcip <span class="nt">-s</span> ip/bytes <span class="nt">-n</span> 10 <span class="s1">'src ip 172.16.0.10'</span>

<span class="c"># Example output:</span>
<span class="c"># Date flow start        Duration Proto      Src IP Addr:Port          Dst IP Addr:Port   Packets    Bytes Flows</span>
<span class="c"># 2026-06-02 10:00:05.123 10.000 TCP   172.16.0.10:45678 -&gt; 10.0.0.5:8080      100    12000     1</span>
<span class="c"># 2026-06-02 10:00:10.456 12.000 UDP   172.16.0.10:12345 -&gt; 10.0.0.8:53        20      500     1</span>
</code></pre>
</div>
<p>Thanks to NetFlow data, I could much faster understand whether a slowdown on an operator screen was due to a sudden high traffic on the network or a problem on the server. In fact, on one occasion, we detected an internal threat trying to infiltrate the production network thanks to high NetFlow traffic going to abnormal ports. When setting up the ZTNA (Zero Trust Network Access) architecture, I also continuously verified egress control with NetFlow data. Seeing which internal resource was sending how much traffic to which external destination ensured that security policies were working correctly. I covered this topic in more detail in my post [related: Zero Trust Network Access architecture and implementation steps].</p>
<p>These two scenarios clearly show that SNMP and NetFlow have distinct use cases that cannot be interchanged. One is indispensable for general health, the other for detailed traffic analysis.</p>
<h2>
<p>  Evaluation from a Security and Performance Perspective<br />
</p></h2>
<p>When choosing network monitoring solutions, it&#8217;s important to consider not only the data type but also the security and performance implications. Both protocols have their own advantages and disadvantages.</p>
<p><strong>SNMP Security:</strong><br />
As I mentioned before, SNMPv1 and v2c versions are weak in terms of security. The unencrypted transmission of <code>community string</code>s across the network allows an attacker to gain sensitive information about the device using tools like <code>snmpwalk</code>. In an internal banking platform, during a security audit, we found that an old switch was still accessible with the default <code>public</code> community string. This was a vulnerability sufficient for a potential insider threat to map the entire network. SNMPv3, however, offers authentication (MD5/SHA) and encryption (DES/AES) mechanisms to close these security gaps, but its configuration is more cumbersome.</p>
<blockquote>
<p><strong><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Using SNMPv3</strong></p>
<p>If you must use SNMP and your devices support it, always prefer SNMPv3. Enable authentication and encryption features to secure your network device information. Additionally, defining ACLs (Access Control Lists) that restrict SNMP access only to the monitoring server&#8217;s IP address provides an extra layer of security.</p>
</blockquote>
<p><strong>NetFlow Security:</strong><br />
NetFlow data itself contains sensitive information, such as which IP address sends how much traffic to which ports, and must be carefully protected. The security of the collector server is critical for the integrity and confidentiality of the collected data. If an attacker gains access to the NetFlow collector, they can see all traffic patterns and potential vulnerabilities on the network. Therefore, NetFlow collectors are typically kept in isolated network segments and protected with strict access controls. Furthermore, in ZTNA (Zero Trust Network Access) architectures, NetFlow is used as a powerful evidence tool to detect unauthorized outbound connections by monitoring egress traffic.</p>
<p><strong>Performance Impacts:</strong></p>
<ul>
<li>  <strong>SNMP:</strong> Creates a load on the network and device depending on the polling frequency. Very frequent polling can increase CPU load, especially on low-power devices, and consume network bandwidth. In my experience, polling hundreds of devices every second strained an average monitoring server and network traffic. This situation creates a need for a kind of &#8220;self-protection&#8221; mechanism, similar to <code>journald</code>&#8216;s <code>RateLimitIntervalSec</code> setting in Linux.</li>
<li>  <strong>NetFlow:</strong> Requires CPU resources on the network device to process and send flows to the collector. Especially on high-traffic routers, this added overhead can affect the device&#8217;s primary tasks (routing). On the collector side, high disk I/O, memory, and CPU capacity are needed to store, process, and make thousands of incoming flow records queryable. On one occasion, when I enabled NetFlow on a 10Gbps switch, I saw the CPU usage increase by 15%, and this started to affect critical routing operations. Therefore, it&#8217;s necessary to carefully evaluate the device&#8217;s performance profile before enabling NetFlow.</li>
</ul>
<p>Both protocols can create performance bottlenecks if not configured correctly. The important thing is to achieve maximum visibility with the minimum resources required to meet our monitoring needs.</p>
<h2>
<p>  My Choice: A Hybrid Approach and Looking to the Future<br />
</p></h2>
<p>If twenty years of experience has taught me anything, it&#8217;s that there is rarely a single &#8220;best&#8221; solution in the world of technology. The choice between SNMP and NetFlow is exactly such a situation. My clear position is to view these two protocols not as rivals, but as complements. The most effective approach to network monitoring is to use a hybrid model.</p>
<p><strong>Fundamentals of My Hybrid Approach:</strong></p>
<ol>
<li>
<p><strong>SNMP for General Health and System Metrics:</strong></p>
<ul>
<li>  I monitor the general health status, port statuses, and total traffic counters of all my network devices (switches, routers, firewalls) and servers (Linux services, CPU, RAM, disk usage) using SNMPv3 at regular intervals (usually 1-5 minutes).</li>
<li>  With this data, I create basic alarms (port down, CPU over 90%, disk full 80%).</li>
<li>  I track long-term trends for capacity planning using this data.</li>
</ul>
</li>
<li>
<p><strong>NetFlow/IPFIX for In-depth Traffic Analysis and Security:</strong></p>
<ul>
<li>  I enable NetFlow/IPFIX export on critical routers and core switches. This is indispensable for monitoring inter-VLAN traffic, flows at company egress points, and communication within critical server farms.</li>
<li>  I use NetFlow data to detect abnormal traffic patterns (DDoS attacks, port scans, unauthorized internal communications).</li>
<li>  I analyze NetFlow data to verify whether QoS (Quality of Service) policies are working correctly. For example, I can check with this data whether DSCP marking is being transmitted correctly end-to-end. Once, I saw with NetFlow that the quality of voice packets was degrading; the reason was incorrect DSCP re-marking on the router.</li>
<li>  I quickly identify the source of performance issues by monitoring application-based bandwidth consumption.</li>
</ul>
</li>
</ol>
<p>This hybrid approach provides me with both a quick overview of my network devices&#8217; general health and the ability to delve into the finest details of traffic flow when needed. Instead of relying on a single tool, I use the strengths of both protocols to monitor my network more comprehensively. This is a much more practical and reliable method than falling into the misconception of &#8220;I can do everything with one tool.&#8221;</p>
<p><strong>Looking to the Future:</strong><br />
Network monitoring technologies are also constantly evolving. Technologies like eBPF (extended Berkeley Packet Filter) offer the ability to collect flow-like data at the Linux kernel level, providing a similar depth to NetFlow at the host level. Furthermore, artificial intelligence and machine learning algorithms have the potential to automatically detect anomalies by working on collected SNMP and NetFlow data without the need for manual threshold definitions. In one of my side projects, I use similar AI models for anomaly detection in financial calculators. This will make network monitoring solutions more proactive and predictive in the future.</p>
<p>My clear position: In network monitoring, &#8220;SNMP or NetFlow?&#8221;</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/snmp-or-netflow-in-network-monitoring-why-does-the-choice-remain/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How I Tested Every Major Multimodal AI Model in 2026 — And Which One Actually Saved My Wallet</title>
		<link>https://codango.com/how-i-tested-every-major-multimodal-ai-model-in-2026-and-which-one-actually-saved-my-wallet/</link>
					<comments>https://codango.com/how-i-tested-every-major-multimodal-ai-model-in-2026-and-which-one-actually-saved-my-wallet/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 01:25:05 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/how-i-tested-every-major-multimodal-ai-model-in-2026-and-which-one-actually-saved-my-wallet/</guid>

					<description><![CDATA[Honestly, I gotta say, when I first started digging into multimodal AI this year, I was expecting everything to be either crazy expensive or kinda mediocre. You know how it <a class="more-link" href="https://codango.com/how-i-tested-every-major-multimodal-ai-model-in-2026-and-which-one-actually-saved-my-wallet/">Continue reading <span class="screen-reader-text">  How I Tested Every Major Multimodal AI Model in 2026 — And Which One Actually Saved My Wallet</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Honestly, I gotta say, when I first started digging into multimodal AI this year, I was expecting everything to be either crazy expensive or kinda mediocre. You know how it goes — every company claims their model is &#8220;revolutionary&#8221; and &#8220;game-changing.&#8221; But after spending way too many late nights running tests, I&#8217;ve got some real answers for you.</p>
<p>Let me cut the BS: I&#8217;m an indie hacker who builds tools for small teams, not some enterprise with infinite cloud credits. So when I say I tested these models, I mean I actually paid for every single API call out of my own pocket. Heres what I found after analyzing thousands of images and audio files.</p>
<h2>
<p>  The Models I Actually Tested (No Fluff)<br />
</p></h2>
<p>I&#8217;m gonna be real with you — not every multimodal model is worth your time. I tested 9 different models through Global API, and some of them surprised me. Here&#8217;s the complete lineup:</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Model</th>
<th>Provider</th>
<th>What It Does</th>
<th>Price per Million Output Tokens</th>
<th>Context Window</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Qwen3-VL-32B</strong></td>
<td>Qwen</td>
<td>Vision + Text</td>
<td>$0.52</td>
<td>32K</td>
</tr>
<tr>
<td><strong>Qwen3-VL-30B-A3B</strong></td>
<td>Qwen</td>
<td>Vision + Text</td>
<td>$0.52</td>
<td>32K</td>
</tr>
<tr>
<td><strong>Qwen3-VL-8B</strong></td>
<td>Qwen</td>
<td>Vision + Text</td>
<td>$0.50</td>
<td>32K</td>
</tr>
<tr>
<td><strong>Qwen3-Omni-30B</strong></td>
<td>Qwen</td>
<td>Image + Audio + Video + Text</td>
<td>$0.52</td>
<td>32K</td>
</tr>
<tr>
<td><strong>GLM-4.6V</strong></td>
<td>Zhipu</td>
<td>Vision + Text</td>
<td>$0.80</td>
<td>32K</td>
</tr>
<tr>
<td><strong>GLM-4.5V</strong></td>
<td>Zhipu</td>
<td>Vision + Text</td>
<td>$0.01</td>
<td>32K</td>
</tr>
<tr>
<td><strong>Hunyuan-Vision</strong></td>
<td>Tencent</td>
<td>Vision + Text</td>
<td>$1.20</td>
<td>32K</td>
</tr>
<tr>
<td><strong>Hunyuan-Turbo-Vision</strong></td>
<td>Tencent</td>
<td>Vision + Text</td>
<td>$1.20</td>
<td>32K</td>
</tr>
<tr>
<td><strong>Doubao-Seed-2.0-Pro</strong></td>
<td>ByteDance</td>
<td>Vision + Text</td>
<td>$3.00</td>
<td>128K</td>
</tr>
</tbody>
</table>
</div>
<p>Yeah, I know — prices range from basically free to &#8220;holy crap, that&#8217;s expensive.&#8221; But trust me, the cheap ones sometimes punch way above their weight.</p>
<h2>
<p>  My Image Testing Setup (Or: How I Burned Through $200 in a Weekend)<br />
</p></h2>
<p>I wanted to test real-world scenarios, not just stock photos of cats. So I grabbed random images from my phone, some documents with mixed Chinese-English text, screenshots of code, and even a few charts I made in Excel (I know, thrilling stuff).</p>
<p>Here&#8217;s the Python code I used for all my tests — you can literally copy-paste this and run it:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">import</span> <span class="n">requests</span>
<span class="kn">import</span> <span class="n">json</span>

<span class="c1"># Global API endpoint — works for all models
</span><span class="n">url</span> <span class="o">=</span> <span class="sh">"</span><span class="s">https://global-apis.com/v1/chat/completions</span><span class="sh">"</span>

<span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">Authorization</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Bearer YOUR_API_KEY_HERE</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">Content-Type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">application/json</span><span class="sh">"</span>
<span class="p">}</span>

<span class="c1"># Example: Qwen3-VL-32B analyzing a street photo
</span><span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Qwen/Qwen3-VL-32B-Instruct</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">messages</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
                <span class="p">{</span>
                    <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">,</span>
                    <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Describe everything you see in this image, including objects, text, brands, and people.</span><span class="sh">"</span>
                <span class="p">},</span>
                <span class="p">{</span>
                    <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">image_url</span><span class="sh">"</span><span class="p">,</span>
                    <span class="sh">"</span><span class="s">image_url</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
                        <span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">https://example.com/street-scene.jpg</span><span class="sh">"</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">]</span>
        <span class="p">}</span>
    <span class="p">],</span>
    <span class="sh">"</span><span class="s">max_tokens</span><span class="sh">"</span><span class="p">:</span> <span class="mi">1024</span>
<span class="p">}</span>

<span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="n">payload</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">json</span><span class="p">()[</span><span class="sh">"</span><span class="s">choices</span><span class="sh">"</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="sh">"</span><span class="s">message</span><span class="sh">"</span><span class="p">][</span><span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">])</span>
</code></pre>
</div>
<p>Pretty straightforward, right? The cool thing about Global API is that you swap the model name and it just works. No changing endpoints, no different auth headers.</p>
<h2>
<p>  Test 1: Object Recognition — The Street Scene Challenge<br />
</p></h2>
<p>I took a photo of a busy street in Shanghai — think neon signs, food stalls, people, bicycles, and a million little details. I wanted to see which model could actually <em>see</em> everything.</p>
<p><strong>Qwen3-VL-32B</strong> absolutely crushed it. I&#8217;m not kidding — it identified 15+ distinct objects, including specific brand names on storefronts, text on a bus schedule, and even the type of dumplings being sold at a stall. It was like having a superpower.</p>
<p><strong>GLM-4.6V</strong> came in second, but only because it was slightly better at recognizing Chinese characters from weird angles. Makes sense since it&#8217;s built by a Chinese company.</p>
<p><strong>Qwen3-Omni-30B</strong> was good but noticeably less detailed than the dedicated vision models. It&#8217;s like the jack-of-all-trades — does everything okay but not great at any one thing.</p>
<p>The budget models? <strong>GLM-4.5V</strong> at $0.01/M got the broad strokes right — &#8220;street with people and shops&#8221; — but missed all the fun details. <strong>Hunyuan-Vision</strong> was a disappointment at $1.20. It missed small objects and got some text wrong.</p>
<h2>
<p>  Test 2: OCR — The Multi-Language Nightmare<br />
</p></h2>
<p>This is where things got interesting. I gave each model a document with English on top, Chinese in the middle, and a mix of both in a table.</p>
<p><strong>Qwen3-VL-32B</strong> was flawless — perfect extraction in both languages, even from a slightly blurry photo. I actually double-checked every single character.</p>
<p><strong>GLM-4.6V</strong> matched it on Chinese OCR but was a tiny bit worse on English. Still, for Chinese-language documents, this might actually be the better choice.</p>
<p><strong>Hunyuan-Vision</strong>&#8230; ugh. It made mistakes on mixed-language content, like reading &#8220;Global&#8221; as &#8220;Globai&#8221; and &#8220;公司&#8221; as &#8220;公司&#8221; (got it right actually, but missed the accent mark). Not great for $1.20.</p>
<h2>
<p>  Test 3: Chart Analysis — Because Spreadsheets Are My Life<br />
</p></h2>
<p>I created a bar chart showing quarterly revenue for a fake company with 8 bars, a trend line, and some annotations.</p>
<p><strong>Qwen3-VL-32B</strong> extracted every data point perfectly and even noticed the trend line was misleading (it was, I made it that way on purpose). The formatting was clean and readable.</p>
<p><strong>GLM-4.6V</strong> got the data right but described the chart in a more verbose way. Not bad if you want a narrative instead of raw numbers.</p>
<p><strong>Qwen3-Omni-30B</strong> was solid but took longer to respond — like a second or two more than the vision-only models. Not a dealbreaker, but noticeable.</p>
<h2>
<p>  Test 4: Code Screenshot to Actual Code (My Favorite)<br />
</p></h2>
<p>As a developer, this is the use case that excites me most. I took a screenshot of a Python function that had some complex list comprehensions and lambda functions.</p>
<p><strong>Qwen3-VL-32B</strong> converted it with 95% accuracy — it got the indentation right, preserved special characters, and even kept the comments. I only had to fix one variable name.</p>
<p><strong>Qwen3-Omni-30B</strong> was 92% accurate but took noticeably longer. Like, 3 seconds vs 1.5 seconds. When you&#8217;re in flow state, those seconds matter.</p>
<p><strong>GLM-4.6V</strong> was 90% accurate but had some formatting issues — it sometimes added extra spaces or removed line breaks.</p>
<h2>
<p>  Audio Processing: The Omni Model&#8217;s Party Trick<br />
</p></h2>
<p>Only <strong>Qwen3-Omni-30B</strong> supports audio input, so this section is short but sweet. I tested it with:</p>
<ul>
<li>A recording of someone speaking Mandarin</li>
<li>A music clip with vocals</li>
<li>An audio file with background noise</li>
</ul>
<p>The speech-to-text was EXCELLENT — it handled multiple languages and even got the accent right. Audio Q&amp;A worked surprisingly well (&#8220;What&#8217;s being said in this recording?&#8221; — it answered correctly). Emotion detection was hit or miss — it correctly identified &#8220;angry&#8221; and &#8220;excited&#8221; but missed &#8220;sarcastic&#8221; (which, honestly, is hard for humans too).</p>
<p>Here&#8217;s how you use audio with it:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="c1"># Qwen3-Omni audio input example
</span><span class="n">payload</span> <span class="o">=</span> <span class="p">{</span>
    <span class="sh">"</span><span class="s">model</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Qwen/Qwen3-Omni-30B-A3B-Instruct</span><span class="sh">"</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">messages</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="sh">"</span><span class="s">role</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">user</span><span class="sh">"</span><span class="p">,</span>
            <span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">:</span> <span class="p">[</span>
                <span class="p">{</span>
                    <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">,</span>
                    <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">Transcribe this audio and describe the speaker</span><span class="sh">'</span><span class="s">s emotion</span><span class="sh">"</span>
                <span class="p">},</span>
                <span class="p">{</span>
                    <span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">audio_url</span><span class="sh">"</span><span class="p">,</span>
                    <span class="sh">"</span><span class="s">audio_url</span><span class="sh">"</span><span class="p">:</span> <span class="p">{</span>
                        <span class="sh">"</span><span class="s">url</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">https://example.com/meeting-recording.mp3</span><span class="sh">"</span>
                    <span class="p">}</span>
                <span class="p">}</span>
            <span class="p">]</span>
        <span class="p">}</span>
    <span class="p">],</span>
    <span class="sh">"</span><span class="s">max_tokens</span><span class="sh">"</span><span class="p">:</span> <span class="mi">1024</span>
<span class="p">}</span>

<span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="n">headers</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="n">payload</span><span class="p">)</span>
<span class="nf">print</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">json</span><span class="p">()[</span><span class="sh">"</span><span class="s">choices</span><span class="sh">"</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="sh">"</span><span class="s">message</span><span class="sh">"</span><span class="p">][</span><span class="sh">"</span><span class="s">content</span><span class="sh">"</span><span class="p">])</span>
</code></pre>
</div>
<h2>
<p>  The Real Talk: Pricing and Value<br />
</p></h2>
<p>Here&#8217;s where I geek out about numbers. Because as an indie hacker, I care about <em>cost per result</em>, not just <em>cost per token</em>.</p>
<div class="table-wrapper-paragraph">
<table>
<thead>
<tr>
<th>Model</th>
<th>$/M Output</th>
<th>Cost for 1,000 Image Analyses</th>
<th>Monthly Cost (10K images)</th>
</tr>
</thead>
<tbody>
<tr>
<td>GLM-4.5V</td>
<td>$0.01</td>
<td>~$0.05</td>
<td>$0.50</td>
</tr>
<tr>
<td>Qwen3-VL-8B</td>
<td>$0.50</td>
<td>~$2.50</td>
<td>$25</td>
</tr>
<tr>
<td><strong>Qwen3-VL-32B</strong></td>
<td><strong>$0.52</strong></td>
<td><strong>~$2.60</strong></td>
<td><strong>$26</strong></td>
</tr>
<tr>
<td>Qwen3-Omni-30B</td>
<td>$0.52</td>
<td>~$2.60 (+ audio)</td>
<td>$26</td>
</tr>
<tr>
<td>GLM-4.6V</td>
<td>$0.80</td>
<td>~$4.00</td>
<td>$40</td>
</tr>
<tr>
<td>Hunyuan-Vision</td>
<td>$1.20</td>
<td>~$6.00</td>
<td>$60</td>
</tr>
<tr>
<td>Doubao-Seed-2.0-Pro</td>
<td>$3.00</td>
<td>~$15.00</td>
<td>$150</td>
</tr>
</tbody>
</table>
</div>
<p>See that huge gap? <strong>GLM-4.5V</strong> at $0.01 is basically free — but you get what you pay for in accuracy. For serious work, <strong>Qwen3-VL-32B</strong> at $0.52 is the sweet spot. It&#8217;s 50 times cheaper than Doubao-Seed-2.0-Pro and honestly performs better in most tests.</p>
<h2>
<p>  My Verdict (After Way Too Much Testing)<br />
</p></h2>
<p>If you&#8217;re building something real — not just experimenting — here&#8217;s what I&#8217;d recommend:</p>
<p><strong>For pure vision tasks:</strong> Go with <strong>Qwen3-VL-32B</strong>. It&#8217;s the best balance of accuracy and price. I&#8217;m using it in my own projects right now.</p>
<p><strong>For Chinese-language content:</strong> <strong>GLM-4.6V</strong> edges ahead slightly, but you pay 50% more. Worth it if accuracy matters more than budget.</p>
<p><strong>If you need audio too:</strong> <strong>Qwen3-Omni-30B</strong> is your only real option, and it&#8217;s surprisingly good. Just be patient with response times.</p>
<p><strong>On a shoestring budget:</strong> <strong>GLM-4.5V</strong> at $0.01/M is fine for prototyping. Just don&#8217;t ship it to production without serious testing.</p>
<h2>
<p>  What I&#8217;m Building Next<br />
</p></h2>
<p>I&#8217;m working on a tool that automatically categorizes product photos for e-commerce stores. My stack? Qwen3-VL-32B for vision, Global API for the connection, and a simple Flask backend. It costs me about $2 per day to process 1,000 images. That&#8217;s insane value.</p>
<p>If you&#8217;re curious about trying these models yourself, check out <strong>Global API</strong> — it&#8217;s where I route all my calls. One endpoint, all the models, no headaches. I&#8217;m not affiliated with them, I just hate managing 10 different API keys.</p>
<p>Honestly, I gotta say, 2026 is the year multimodal AI stopped being a gimmick and started being actually useful for builders like us. Go test it yourself — you might be surprised what these cheap models can do.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/how-i-tested-every-major-multimodal-ai-model-in-2026-and-which-one-actually-saved-my-wallet/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		<enclosure url="https://example.com/meeting-recording.mp3" length="0" type="audio/mpeg" />

			</item>
		<item>
		<title>Building a Service Marketplace with Django: Lessons from Netfix</title>
		<link>https://codango.com/building-a-service-marketplace-with-django-lessons-from-netfix/</link>
					<comments>https://codango.com/building-a-service-marketplace-with-django-lessons-from-netfix/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 20:15:11 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/building-a-service-marketplace-with-django-lessons-from-netfix/</guid>

					<description><![CDATA[As developers, we&#8217;re constantly searching for the right tools to bring our ideas to life efficiently. When I decided to build Netfix, a service marketplace connecting companies with customers, I <a class="more-link" href="https://codango.com/building-a-service-marketplace-with-django-lessons-from-netfix/">Continue reading <span class="screen-reader-text">  Building a Service Marketplace with Django: Lessons from Netfix</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>As developers, we&#8217;re constantly searching for the right tools to bring our ideas to life efficiently. When I decided to build Netfix, a service marketplace connecting companies with customers, I chose Django &#8211; and discovered features that make it uniquely powerful for complex applications.<br />
In this article, I&#8217;ll share deep technical insights from building Netfix, highlighting Django features that aren&#8217;t commonly covered in tutorials but can dramatically improve your development workflow</p>
<h2>
<p>  Project Overview: Netfix<br />
</p></h2>
<p>Netfix is a platform where:</p>
<ul>
<li>Companies create profiles and list their services</li>
<li>Customers browse and request services</li>
<li>Each entity has personalized dashboards</li>
<li>Analytics track and display trending services</li>
<li>The system handles complex relationships between users, services, and requests</li>
</ul>
<p>Let&#8217;s dive into some of the most powerful features I leveraged</p>
<h3>
<p>  1. Custom User Models &#8211; The Right Way<br />
</p></h3>
<p>Django&#8217;s default user model works for simple applications, but for Netfix, I needed to support different user types with specific attributes. Here&#8217;s how I implemented a custom user model:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">django.contrib.auth.models</span> <span class="kn">import</span> <span class="n">AbstractBaseUser</span><span class="p">,</span> <span class="n">PermissionsMixin</span><span class="p">,</span> <span class="n">BaseUserManager</span>
<span class="kn">from</span> <span class="n">django.db</span> <span class="kn">import</span> <span class="n">models</span>

<span class="k">class</span> <span class="nc">UserManager</span><span class="p">(</span><span class="n">BaseUserManager</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">create_user</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">):</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">email</span><span class="p">:</span>
            <span class="k">raise</span> <span class="nc">ValueError</span><span class="p">(</span><span class="sh">'</span><span class="s">Email is required</span><span class="sh">'</span><span class="p">)</span>
        <span class="n">email</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">normalize_email</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
        <span class="n">user</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">model</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">)</span>
        <span class="n">user</span><span class="p">.</span><span class="nf">set_password</span><span class="p">(</span><span class="n">password</span><span class="p">)</span>
        <span class="n">user</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">using</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">_db</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">user</span>

    <span class="k">def</span> <span class="nf">create_superuser</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">):</span>
        <span class="n">extra_fields</span><span class="p">.</span><span class="nf">setdefault</span><span class="p">(</span><span class="sh">'</span><span class="s">is_staff</span><span class="sh">'</span><span class="p">,</span> <span class="bp">True</span><span class="p">)</span>
        <span class="n">extra_fields</span><span class="p">.</span><span class="nf">setdefault</span><span class="p">(</span><span class="sh">'</span><span class="s">is_superuser</span><span class="sh">'</span><span class="p">,</span> <span class="bp">True</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nf">create_user</span><span class="p">(</span><span class="n">email</span><span class="p">,</span> <span class="n">password</span><span class="p">,</span> <span class="o">**</span><span class="n">extra_fields</span><span class="p">)</span>

<span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="n">AbstractBaseUser</span><span class="p">,</span> <span class="n">PermissionsMixin</span><span class="p">):</span>
    <span class="n">USER_TYPE_CHOICES</span> <span class="o">=</span> <span class="p">(</span>
        <span class="p">(</span><span class="sh">'</span><span class="s">customer</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">Customer</span><span class="sh">'</span><span class="p">),</span>
        <span class="p">(</span><span class="sh">'</span><span class="s">company</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">Company</span><span class="sh">'</span><span class="p">),</span>
    <span class="p">)</span>

    <span class="n">email</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">EmailField</span><span class="p">(</span><span class="n">unique</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">name</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">255</span><span class="p">)</span>
    <span class="n">user_type</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span> <span class="n">choices</span><span class="o">=</span><span class="n">USER_TYPE_CHOICES</span><span class="p">)</span>
    <span class="n">is_active</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">BooleanField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">is_staff</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">BooleanField</span><span class="p">(</span><span class="n">default</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
    <span class="n">date_joined</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">DateTimeField</span><span class="p">(</span><span class="n">auto_now_add</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="n">objects</span> <span class="o">=</span> <span class="nc">UserManager</span><span class="p">()</span>

    <span class="n">USERNAME_FIELD</span> <span class="o">=</span> <span class="sh">'</span><span class="s">email</span><span class="sh">'</span>
    <span class="n">REQUIRED_FIELDS</span> <span class="o">=</span> <span class="p">[</span><span class="sh">'</span><span class="s">name</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">user_type</span><span class="sh">'</span><span class="p">]</span>

    <span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">email</span>
</code></pre>
</div>
<p>Then I created separate profile models for each user type:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="c1"># accounts/models.py
</span><span class="k">class</span> <span class="nc">CompanyProfile</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">OneToOneField</span><span class="p">(</span><span class="n">User</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="p">.</span><span class="n">CASCADE</span><span class="p">,</span> <span class="n">related_name</span><span class="o">=</span><span class="sh">'</span><span class="s">company_profile</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">company_name</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">255</span><span class="p">)</span>
    <span class="n">description</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">TextField</span><span class="p">()</span>
    <span class="n">logo</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">ImageField</span><span class="p">(</span><span class="n">upload_to</span><span class="o">=</span><span class="sh">'</span><span class="s">company_logos/</span><span class="sh">'</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">website</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">URLField</span><span class="p">(</span><span class="n">null</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">company_name</span>

<span class="k">class</span> <span class="nc">CustomerProfile</span><span class="p">(</span><span class="n">models</span><span class="p">.</span><span class="n">Model</span><span class="p">):</span>
    <span class="n">user</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">OneToOneField</span><span class="p">(</span><span class="n">User</span><span class="p">,</span> <span class="n">on_delete</span><span class="o">=</span><span class="n">models</span><span class="p">.</span><span class="n">CASCADE</span><span class="p">,</span> <span class="n">related_name</span><span class="o">=</span><span class="sh">'</span><span class="s">customer_profile</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">phone_number</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">CharField</span><span class="p">(</span><span class="n">max_length</span><span class="o">=</span><span class="mi">20</span><span class="p">,</span> <span class="n">null</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
    <span class="n">address</span> <span class="o">=</span> <span class="n">models</span><span class="p">.</span><span class="nc">TextField</span><span class="p">(</span><span class="n">null</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span> <span class="n">blank</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">name</span>
</code></pre>
</div>
<p><strong>Pro tip:</strong> When creating a custom user model, do it at the beginning of your project. Changing the user model mid-project is extremely challenging.</p>
<h3>
<p>  2. Signals for Automatic Profile Creation<br />
</p></h3>
<p>One feature I love about Django is signals. They allow you to trigger actions when certain events occur. I used signals to automatically create appropriate profiles when a user registers:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">django.db.models.signals</span> <span class="kn">import</span> <span class="n">post_save</span>
<span class="kn">from</span> <span class="n">django.dispatch</span> <span class="kn">import</span> <span class="n">receiver</span>
<span class="kn">from</span> <span class="n">.models</span> <span class="kn">import</span> <span class="n">User</span><span class="p">,</span> <span class="n">CompanyProfile</span><span class="p">,</span> <span class="n">CustomerProfile</span>

<span class="nd">@receiver</span><span class="p">(</span><span class="n">post_save</span><span class="p">,</span> <span class="n">sender</span><span class="o">=</span><span class="n">User</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">create_user_profile</span><span class="p">(</span><span class="n">sender</span><span class="p">,</span> <span class="n">instance</span><span class="p">,</span> <span class="n">created</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">created</span><span class="p">:</span>
        <span class="k">if</span> <span class="n">instance</span><span class="p">.</span><span class="n">user_type</span> <span class="o">==</span> <span class="sh">'</span><span class="s">company</span><span class="sh">'</span><span class="p">:</span>
            <span class="n">CompanyProfile</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">user</span><span class="o">=</span><span class="n">instance</span><span class="p">)</span>
        <span class="k">elif</span> <span class="n">instance</span><span class="p">.</span><span class="n">user_type</span> <span class="o">==</span> <span class="sh">'</span><span class="s">customer</span><span class="sh">'</span><span class="p">:</span>
            <span class="n">CustomerProfile</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span><span class="n">user</span><span class="o">=</span><span class="n">instance</span><span class="p">)</span>

<span class="nd">@receiver</span><span class="p">(</span><span class="n">post_save</span><span class="p">,</span> <span class="n">sender</span><span class="o">=</span><span class="n">User</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">save_user_profile</span><span class="p">(</span><span class="n">sender</span><span class="p">,</span> <span class="n">instance</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">instance</span><span class="p">.</span><span class="n">user_type</span> <span class="o">==</span> <span class="sh">'</span><span class="s">company</span><span class="sh">'</span><span class="p">:</span>
        <span class="n">instance</span><span class="p">.</span><span class="n">company_profile</span><span class="p">.</span><span class="nf">save</span><span class="p">()</span>
    <span class="k">elif</span> <span class="n">instance</span><span class="p">.</span><span class="n">user_type</span> <span class="o">==</span> <span class="sh">'</span><span class="s">customer</span><span class="sh">'</span><span class="p">:</span>
        <span class="n">instance</span><span class="p">.</span><span class="n">customer_profile</span><span class="p">.</span><span class="nf">save</span><span class="p">()</span>
</code></pre>
</div>
<h3>
<p>  3. Advanced QuerySets with Annotations for Service Analytics<br />
</p></h3>
<p>For the trending services feature, I needed to count service requests and display them by popularity. Django&#8217;s ORM provides powerful annotation capabilities:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">django.db.models</span> <span class="kn">import</span> <span class="n">Count</span><span class="p">,</span> <span class="n">F</span><span class="p">,</span> <span class="n">ExpressionWrapper</span><span class="p">,</span> <span class="n">fields</span>
<span class="kn">from</span> <span class="n">django.db.models.functions</span> <span class="kn">import</span> <span class="n">Now</span><span class="p">,</span> <span class="n">ExtractDay</span>
<span class="kn">from</span> <span class="n">datetime</span> <span class="kn">import</span> <span class="n">timedelta</span>
<span class="kn">from</span> <span class="n">.models</span> <span class="kn">import</span> <span class="n">Service</span><span class="p">,</span> <span class="n">ServiceRequest</span>

<span class="k">def</span> <span class="nf">trending_services</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
    <span class="c1"># Calculate trending services based on request count in last 30 days
</span>    <span class="n">thirty_days_ago</span> <span class="o">=</span> <span class="n">timezone</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span> <span class="o">-</span> <span class="nf">timedelta</span><span class="p">(</span><span class="n">days</span><span class="o">=</span><span class="mi">30</span><span class="p">)</span>

    <span class="n">trending</span> <span class="o">=</span> <span class="n">Service</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nf">annotate</span><span class="p">(</span>
        <span class="n">request_count</span><span class="o">=</span><span class="nc">Count</span><span class="p">(</span><span class="sh">'</span><span class="s">servicerequest</span><span class="sh">'</span><span class="p">,</span> 
                           <span class="nb">filter</span><span class="o">=</span><span class="n">models</span><span class="p">.</span><span class="nc">Q</span><span class="p">(</span><span class="n">servicerequest__created_at__gte</span><span class="o">=</span><span class="n">thirty_days_ago</span><span class="p">))</span>
    <span class="p">).</span><span class="nf">order_by</span><span class="p">(</span><span class="sh">'</span><span class="s">-request_count</span><span class="sh">'</span><span class="p">)[:</span><span class="mi">10</span><span class="p">]</span>

    <span class="k">return</span> <span class="nf">render</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="sh">'</span><span class="s">services/trending.html</span><span class="sh">'</span><span class="p">,</span> <span class="p">{</span><span class="sh">'</span><span class="s">trending_services</span><span class="sh">'</span><span class="p">:</span> <span class="n">trending</span><span class="p">})</span>
</code></pre>
</div>
<h3>
<p>  4. Custom Template Tags for Dynamic UI Elements<br />
</p></h3>
<p>One lesser-known Django feature is custom template tags. I created a template tag to display service status with appropriate styling:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">django</span> <span class="kn">import</span> <span class="n">template</span>
<span class="kn">from</span> <span class="n">django.utils.safestring</span> <span class="kn">import</span> <span class="n">mark_safe</span>

<span class="n">register</span> <span class="o">=</span> <span class="n">template</span><span class="p">.</span><span class="nc">Library</span><span class="p">()</span>

<span class="nd">@register.filter</span>
<span class="k">def</span> <span class="nf">status_badge</span><span class="p">(</span><span class="n">status</span><span class="p">):</span>
    <span class="n">colors</span> <span class="o">=</span> <span class="p">{</span>
        <span class="sh">'</span><span class="s">pending</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">warning</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">accepted</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">info</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">in_progress</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">primary</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">completed</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">success</span><span class="sh">'</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">declined</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">danger</span><span class="sh">'</span>
    <span class="p">}</span>
    <span class="n">color</span> <span class="o">=</span> <span class="n">colors</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">status</span><span class="p">,</span> <span class="sh">'</span><span class="s">secondary</span><span class="sh">'</span><span class="p">)</span>
    <span class="k">return</span> <span class="nf">mark_safe</span><span class="p">(</span><span class="sa">f</span><span class="sh">'</span><span class="s">&lt;span class=</span><span class="sh">"</span><span class="s">badge bg-</span><span class="si">{</span><span class="n">color</span><span class="si">}</span><span class="sh">"</span><span class="s">&gt;</span><span class="si">{</span><span class="n">status</span><span class="p">.</span><span class="nf">replace</span><span class="p">(</span><span class="sh">"</span><span class="s">_</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">).</span><span class="nf">title</span><span class="p">()</span><span class="si">}</span><span class="s">&lt;/span&gt;</span><span class="sh">'</span><span class="p">)</span>
</code></pre>
</div>
<p>Then in templates:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight html"><code>{% load service_extras %}

<span class="nt">&lt;td&gt;</span>{{ service_request.status|status_badge }}
</code></pre>
</div>
<h3>
<p>  5. Class-Based Views with Mixins for Controlled Access<br />
</p></h3>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">django.contrib.auth.mixins</span> <span class="kn">import</span> <span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="n">UserPassesTestMixin</span>
<span class="kn">from</span> <span class="n">django.views.generic</span> <span class="kn">import</span> <span class="n">ListView</span><span class="p">,</span> <span class="n">DetailView</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">,</span> <span class="n">UpdateView</span>

<span class="k">class</span> <span class="nc">CompanyRequiredMixin</span><span class="p">(</span><span class="n">UserPassesTestMixin</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">test_func</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">is_authenticated</span> <span class="ow">and</span> <span class="n">self</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">user_type</span> <span class="o">==</span> <span class="sh">'</span><span class="s">company</span><span class="sh">'</span>

<span class="k">class</span> <span class="nc">ServiceCreateView</span><span class="p">(</span><span class="n">LoginRequiredMixin</span><span class="p">,</span> <span class="n">CompanyRequiredMixin</span><span class="p">,</span> <span class="n">CreateView</span><span class="p">):</span>
    <span class="n">model</span> <span class="o">=</span> <span class="n">Service</span>
    <span class="n">fields</span> <span class="o">=</span> <span class="p">[</span><span class="sh">'</span><span class="s">name</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">description</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">price</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">category</span><span class="sh">'</span><span class="p">]</span>
    <span class="n">template_name</span> <span class="o">=</span> <span class="sh">'</span><span class="s">services/service_form.html</span><span class="sh">'</span>

    <span class="k">def</span> <span class="nf">form_valid</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">form</span><span class="p">):</span>
        <span class="n">form</span><span class="p">.</span><span class="n">instance</span><span class="p">.</span><span class="n">company</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">company_profile</span>
        <span class="k">return</span> <span class="nf">super</span><span class="p">().</span><span class="nf">form_valid</span><span class="p">(</span><span class="n">form</span><span class="p">)</span>
</code></pre>
</div>
<h3>
<p>  6. Using Select Related and Prefetch Related for Performance<br />
</p></h3>
<p>One challenge with relational data is the &#8220;N+1 query problem.&#8221; Django&#8217;s select_related and prefetch_related provide elegant solutions:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="c1"># Without optimization - this causes N+1 queries
</span><span class="k">def</span> <span class="nf">service_requests</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
    <span class="n">requests</span> <span class="o">=</span> <span class="n">ServiceRequest</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nf">filter</span><span class="p">(</span><span class="n">service__company__user</span><span class="o">=</span><span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">)</span>
    <span class="c1"># Each time we access requests.service, we make a new query
</span>    <span class="k">return</span> <span class="nf">render</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="sh">'</span><span class="s">services/requests.html</span><span class="sh">'</span><span class="p">,</span> <span class="p">{</span><span class="sh">'</span><span class="s">requests</span><span class="sh">'</span><span class="p">:</span> <span class="n">requests</span><span class="p">})</span>

<span class="c1"># With optimization
</span><span class="k">def</span> <span class="nf">service_requests_optimized</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
    <span class="n">requests</span> <span class="o">=</span> <span class="n">ServiceRequest</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nf">filter</span><span class="p">(</span>
        <span class="n">service__company__user</span><span class="o">=</span><span class="n">request</span><span class="p">.</span><span class="n">user</span>
    <span class="p">).</span><span class="nf">select_related</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">service</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">customer</span><span class="sh">'</span>
    <span class="p">).</span><span class="nf">prefetch_related</span><span class="p">(</span>
        <span class="sh">'</span><span class="s">service__category</span><span class="sh">'</span>
    <span class="p">)</span>
    <span class="c1"># Now all related data is fetched in just 3 queries
</span>    <span class="k">return</span> <span class="nf">render</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="sh">'</span><span class="s">services/requests.html</span><span class="sh">'</span><span class="p">,</span> <span class="p">{</span><span class="sh">'</span><span class="s">requests</span><span class="sh">'</span><span class="p">:</span> <span class="n">requests</span><span class="p">})</span>
</code></pre>
</div>
<h3>
<p>  7. REST Framework ViewSets for API Development<br />
</p></h3>
<p>To provide a robust API for third-party integrations and future platform extensibility, I built a comprehensive API using Django REST Framework&#8217;s ViewSets:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">rest_framework</span> <span class="kn">import</span> <span class="n">viewsets</span><span class="p">,</span> <span class="n">permissions</span>
<span class="kn">from</span> <span class="n">.serializers</span> <span class="kn">import</span> <span class="n">ServiceSerializer</span><span class="p">,</span> <span class="n">ServiceRequestSerializer</span>
<span class="kn">from</span> <span class="n">services.models</span> <span class="kn">import</span> <span class="n">Service</span><span class="p">,</span> <span class="n">ServiceRequest</span>

<span class="k">class</span> <span class="nc">IsCompanyOrReadOnly</span><span class="p">(</span><span class="n">permissions</span><span class="p">.</span><span class="n">BasePermission</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">has_permission</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">request</span><span class="p">,</span> <span class="n">view</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="n">method</span> <span class="ow">in</span> <span class="n">permissions</span><span class="p">.</span><span class="n">SAFE_METHODS</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">True</span>
        <span class="k">return</span> <span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">is_authenticated</span> <span class="ow">and</span> <span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">user_type</span> <span class="o">==</span> <span class="sh">'</span><span class="s">company</span><span class="sh">'</span>

<span class="k">class</span> <span class="nc">ServiceViewSet</span><span class="p">(</span><span class="n">viewsets</span><span class="p">.</span><span class="n">ModelViewSet</span><span class="p">):</span>
    <span class="n">serializer_class</span> <span class="o">=</span> <span class="n">ServiceSerializer</span>
    <span class="n">permission_classes</span> <span class="o">=</span> <span class="p">[</span><span class="n">IsCompanyOrReadOnly</span><span class="p">]</span>

    <span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">queryset</span> <span class="o">=</span> <span class="n">Service</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nf">all</span><span class="p">()</span>
        <span class="n">category</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">query_params</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">category</span><span class="sh">'</span><span class="p">,</span> <span class="bp">None</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">category</span><span class="p">:</span>
            <span class="n">queryset</span> <span class="o">=</span> <span class="n">queryset</span><span class="p">.</span><span class="nf">filter</span><span class="p">(</span><span class="n">category__slug</span><span class="o">=</span><span class="n">category</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">queryset</span>

    <span class="k">def</span> <span class="nf">perform_create</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">serializer</span><span class="p">):</span>
        <span class="n">serializer</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">company</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">company_profile</span><span class="p">)</span>
</code></pre>
</div>
<h3>
<p>  8. Advanced Form Processing with FormSets<br />
</p></h3>
<p>For services with multiple attributes, I used Django&#8217;s formsets:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">django.forms</span> <span class="kn">import</span> <span class="n">inlineformset_factory</span>
<span class="kn">from</span> <span class="n">.models</span> <span class="kn">import</span> <span class="n">Service</span><span class="p">,</span> <span class="n">ServiceAttribute</span>

<span class="k">def</span> <span class="nf">service_create_with_attributes</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
    <span class="n">AttributeFormSet</span> <span class="o">=</span> <span class="nf">inlineformset_factory</span><span class="p">(</span>
        <span class="n">Service</span><span class="p">,</span> <span class="n">ServiceAttribute</span><span class="p">,</span> 
        <span class="n">fields</span><span class="o">=</span><span class="p">(</span><span class="sh">'</span><span class="s">name</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">value</span><span class="sh">'</span><span class="p">),</span> 
        <span class="n">extra</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">can_delete</span><span class="o">=</span><span class="bp">True</span>
    <span class="p">)</span>

    <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="n">method</span> <span class="o">==</span> <span class="sh">'</span><span class="s">POST</span><span class="sh">'</span><span class="p">:</span>
        <span class="n">form</span> <span class="o">=</span> <span class="nc">ServiceForm</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">POST</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">form</span><span class="p">.</span><span class="nf">is_valid</span><span class="p">():</span>
            <span class="n">service</span> <span class="o">=</span> <span class="n">form</span><span class="p">.</span><span class="nf">save</span><span class="p">(</span><span class="n">commit</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
            <span class="n">service</span><span class="p">.</span><span class="n">company</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">company_profile</span>
            <span class="n">service</span><span class="p">.</span><span class="nf">save</span><span class="p">()</span>

            <span class="n">formset</span> <span class="o">=</span> <span class="nc">AttributeFormSet</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">POST</span><span class="p">,</span> <span class="n">instance</span><span class="o">=</span><span class="n">service</span><span class="p">)</span>
            <span class="k">if</span> <span class="n">formset</span><span class="p">.</span><span class="nf">is_valid</span><span class="p">():</span>
                <span class="n">formset</span><span class="p">.</span><span class="nf">save</span><span class="p">()</span>
                <span class="k">return</span> <span class="nf">redirect</span><span class="p">(</span><span class="sh">'</span><span class="s">service-detail</span><span class="sh">'</span><span class="p">,</span> <span class="n">pk</span><span class="o">=</span><span class="n">service</span><span class="p">.</span><span class="n">pk</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">form</span> <span class="o">=</span> <span class="nc">ServiceForm</span><span class="p">()</span>
        <span class="n">formset</span> <span class="o">=</span> <span class="nc">AttributeFormSet</span><span class="p">()</span>

    <span class="k">return</span> <span class="nf">render</span><span class="p">(</span><span class="n">request</span><span class="p">,</span> <span class="sh">'</span><span class="s">services/service_with_attributes.html</span><span class="sh">'</span><span class="p">,</span> <span class="p">{</span>
        <span class="sh">'</span><span class="s">form</span><span class="sh">'</span><span class="p">:</span> <span class="n">form</span><span class="p">,</span>
        <span class="sh">'</span><span class="s">formset</span><span class="sh">'</span><span class="p">:</span> <span class="n">formset</span>
    <span class="p">})</span>
</code></pre>
</div>
<h3>
<p>  9. Leveraging Django Admin for Quick Internal Tools<br />
</p></h3>
<p>Django&#8217;s admin site is incredibly powerful. I customized it for internal management:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code>
<span class="kn">from</span> <span class="n">django.contrib</span> <span class="kn">import</span> <span class="n">admin</span>
<span class="kn">from</span> <span class="n">.models</span> <span class="kn">import</span> <span class="n">Service</span><span class="p">,</span> <span class="n">ServiceRequest</span><span class="p">,</span> <span class="n">Category</span>

<span class="nd">@admin.register</span><span class="p">(</span><span class="n">Service</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ServiceAdmin</span><span class="p">(</span><span class="n">admin</span><span class="p">.</span><span class="n">ModelAdmin</span><span class="p">):</span>
    <span class="n">list_display</span> <span class="o">=</span> <span class="p">(</span><span class="sh">'</span><span class="s">name</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">company</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">price</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">category</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">is_active</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">list_filter</span> <span class="o">=</span> <span class="p">(</span><span class="sh">'</span><span class="s">is_active</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">category</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">company</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">search_fields</span> <span class="o">=</span> <span class="p">(</span><span class="sh">'</span><span class="s">name</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">description</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">company__company_name</span><span class="sh">'</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">get_queryset</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
        <span class="n">qs</span> <span class="o">=</span> <span class="nf">super</span><span class="p">().</span><span class="nf">get_queryset</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">is_superuser</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">qs</span>
        <span class="k">return</span> <span class="n">qs</span><span class="p">.</span><span class="nf">filter</span><span class="p">(</span><span class="n">company__user</span><span class="o">=</span><span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">)</span>

<span class="nd">@admin.register</span><span class="p">(</span><span class="n">ServiceRequest</span><span class="p">)</span>
<span class="k">class</span> <span class="nc">ServiceRequestAdmin</span><span class="p">(</span><span class="n">admin</span><span class="p">.</span><span class="n">ModelAdmin</span><span class="p">):</span>
    <span class="n">list_display</span> <span class="o">=</span> <span class="p">(</span><span class="sh">'</span><span class="s">service</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">customer</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">status</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">created_at</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">list_filter</span> <span class="o">=</span> <span class="p">(</span><span class="sh">'</span><span class="s">status</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">created_at</span><span class="sh">'</span><span class="p">)</span>
    <span class="n">date_hierarchy</span> <span class="o">=</span> <span class="sh">'</span><span class="s">created_at</span><span class="sh">'</span>
</code></pre>
</div>
<h3>
<p>  10. Middleware for Request Tracking<br />
</p></h3>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="kn">from</span> <span class="n">.models</span> <span class="kn">import</span> <span class="n">Service</span><span class="p">,</span> <span class="n">ServiceView</span>
<span class="kn">from</span> <span class="n">django.utils</span> <span class="kn">import</span> <span class="n">timezone</span>

<span class="k">class</span> <span class="nc">ServiceViewMiddleware</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">get_response</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">get_response</span> <span class="o">=</span> <span class="n">get_response</span>

    <span class="k">def</span> <span class="nf">__call__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
        <span class="n">response</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">get_response</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>

        <span class="c1"># Check if this is a service detail page
</span>        <span class="n">path_parts</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">strip</span><span class="p">(</span><span class="sh">'</span><span class="s">/</span><span class="sh">'</span><span class="p">).</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="s">/</span><span class="sh">'</span><span class="p">)</span>
        <span class="k">if</span> <span class="nf">len</span><span class="p">(</span><span class="n">path_parts</span><span class="p">)</span> <span class="o">&gt;=</span> <span class="mi">3</span> <span class="ow">and</span> <span class="n">path_parts</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="sh">'</span><span class="s">services</span><span class="sh">'</span> <span class="ow">and</span> <span class="n">path_parts</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="sh">'</span><span class="s">detail</span><span class="sh">'</span><span class="p">:</span>
            <span class="k">try</span><span class="p">:</span>
                <span class="n">service_id</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">path_parts</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>
                <span class="n">service</span> <span class="o">=</span> <span class="n">Service</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="nb">id</span><span class="o">=</span><span class="n">service_id</span><span class="p">)</span>

                <span class="c1"># Record anonymous view
</span>                <span class="n">ServiceView</span><span class="p">.</span><span class="n">objects</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
                    <span class="n">service</span><span class="o">=</span><span class="n">service</span><span class="p">,</span>
                    <span class="n">ip_address</span><span class="o">=</span><span class="n">self</span><span class="p">.</span><span class="nf">get_client_ip</span><span class="p">(</span><span class="n">request</span><span class="p">),</span>
                    <span class="n">user</span><span class="o">=</span><span class="n">request</span><span class="p">.</span><span class="n">user</span> <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="n">user</span><span class="p">.</span><span class="n">is_authenticated</span> <span class="k">else</span> <span class="bp">None</span><span class="p">,</span>
                    <span class="n">viewed_at</span><span class="o">=</span><span class="n">timezone</span><span class="p">.</span><span class="nf">now</span><span class="p">()</span>
                <span class="p">)</span>
            <span class="nf">except </span><span class="p">(</span><span class="nb">ValueError</span><span class="p">,</span> <span class="n">Service</span><span class="p">.</span><span class="n">DoesNotExist</span><span class="p">):</span>
                <span class="k">pass</span>

        <span class="k">return</span> <span class="n">response</span>

    <span class="k">def</span> <span class="nf">get_client_ip</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">request</span><span class="p">):</span>
        <span class="n">x_forwarded_for</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">META</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">HTTP_X_FORWARDED_FOR</span><span class="sh">'</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">x_forwarded_for</span><span class="p">:</span>
            <span class="n">ip</span> <span class="o">=</span> <span class="n">x_forwarded_for</span><span class="p">.</span><span class="nf">split</span><span class="p">(</span><span class="sh">'</span><span class="s">,</span><span class="sh">'</span><span class="p">)[</span><span class="mi">0</span><span class="p">]</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">ip</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">META</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">'</span><span class="s">REMOTE_ADDR</span><span class="sh">'</span><span class="p">)</span>
</code></pre>
</div>
<p>Don&#8217;t forget to add it to your settings:
</p>
<div class="highlight js-code-highlight">
<pre class="highlight python"><code><span class="n">MIDDLEWARE</span> <span class="o">=</span> <span class="p">[</span>
    <span class="c1"># ... other middleware
</span>    <span class="sh">'</span><span class="s">services.middleware.ServiceViewMiddleware</span><span class="sh">'</span><span class="p">,</span>
<span class="p">]</span>
</code></pre>
</div>
<h3>
<p>  Conclusion<br />
</p></h3>
<p>Building Netfix with Django taught me how powerful and flexible the framework truly is. These advanced features &#8211; from custom user models to complex queries with annotations &#8211; enabled me to create a robust marketplace with clean, maintainable code.<br />
What I appreciate most about Django is how it scales with your knowledge. As you discover more features, you can refactor and improve your application while maintaining backward compatibility</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/building-a-service-marketplace-with-django-lessons-from-netfix/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How to test MCP servers in TypeScript before they break in production</title>
		<link>https://codango.com/how-to-test-mcp-servers-in-typescript-before-they-break-in-production/</link>
					<comments>https://codango.com/how-to-test-mcp-servers-in-typescript-before-they-break-in-production/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 10:20:52 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/how-to-test-mcp-servers-in-typescript-before-they-break-in-production/</guid>

					<description><![CDATA[Your MCP server works on your laptop. The tool calls return the right shapes, the client connects cleanly, the session behaves. Then you deploy it and a client reconnects after <a class="more-link" href="https://codango.com/how-to-test-mcp-servers-in-typescript-before-they-break-in-production/">Continue reading <span class="screen-reader-text">  How to test MCP servers in TypeScript before they break in production</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>Your MCP server works on your laptop. The tool calls return the right shapes, the client connects cleanly, the session behaves. Then you deploy it and a client reconnects after a network hiccup and the session state is gone. Or you scale to two instances and half the requests fail because session IDs resolve to the wrong process. Or someone sends two concurrent requests and the tool handler corrupts shared state.</p>
<p>Testing catches these before your users do. This is a testing playbook for TypeScript MCP servers built on the official SDK.</p>
<h2>
<p>  The demo-to-production gap for MCP servers<br />
</p></h2>
<p>The official TypeScript SDK makes it easy to get something working. A few tool registrations, an <code>McpServer</code> instance, a transport, and you are serving. The problem is that &#8220;working&#8221; in the demo sense and &#8220;working&#8221; in the production sense are different things.</p>
<p>A demo tests one happy path. Production tests edge cases that emerge from real clients: reconnects, concurrent tool calls, malformed inputs, slow downstream APIs, and the transport contract itself. None of those show up in a single manual run against your local instance.</p>
<p>The gap is not a criticism of the SDK. It is a consequence of how easy the SDK makes it to build a server without thinking about what breaks it. A test suite closes the gap before you ship.</p>
<h2>
<p>  What actually breaks: transport, sessions, tool contracts<br />
</p></h2>
<p>Three categories fail most often.</p>
<p><strong>Transport behavior.</strong> The SDK added Streamable HTTP support in version 1.10.0. Under this transport, the server exposes a single HTTP endpoint that handles both POST and GET. Clients use POST for tool calls and GET to open a streaming connection via server sent events. Tests that only exercise stdio miss this entirely.</p>
<p><strong>Session state.</strong> The <code>StreamableHTTPServerTransport</code> is stateful per session. If you store anything in process memory keyed by session ID, a restart or a second instance will drop it. Tests that do not simulate reconnects miss this failure mode.</p>
<p><strong>Tool contracts.</strong> Each tool you register has an input schema and an expected output shape. Tests that call tools with valid inputs only miss the cases where a client sends a slightly wrong shape or a downstream API returns something unexpected.</p>
<h2>
<p>  Unit-testing tools and resources in isolation<br />
</p></h2>
<p>The cleanest place to start is the tool handler itself, before any transport is involved.</p>
<p>Each tool handler is a function that takes validated input and returns a result. Extract the handler logic from the <code>server.tool()</code> registration so you can call it directly in tests.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="c1">// tool-handlers.ts</span>
<span class="k">export</span> <span class="k">async</span> <span class="kd">function</span> <span class="nf">getItemHandler</span><span class="p">(</span><span class="nx">input</span><span class="p">:</span> <span class="p">{</span> <span class="nl">id</span><span class="p">:</span> <span class="kr">string</span> <span class="p">})</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">item</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetchItem</span><span class="p">(</span><span class="nx">input</span><span class="p">.</span><span class="nx">id</span><span class="p">);</span>
  <span class="k">return</span> <span class="p">{</span> <span class="na">content</span><span class="p">:</span> <span class="p">[{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">text</span><span class="dl">"</span><span class="p">,</span> <span class="na">text</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span> <span class="p">}]</span> <span class="p">};</span>
<span class="p">}</span>
</code></pre>
</div>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="c1">// getItem.test.ts</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">getItemHandler</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./tool-handlers</span><span class="dl">"</span><span class="p">;</span>

<span class="nf">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">returns item content</span><span class="dl">"</span><span class="p">,</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">getItemHandler</span><span class="p">({</span> <span class="na">id</span><span class="p">:</span> <span class="dl">"</span><span class="s2">abc</span><span class="dl">"</span> <span class="p">});</span>
  <span class="nf">expect</span><span class="p">(</span><span class="nx">result</span><span class="p">.</span><span class="nx">content</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="kd">type</span><span class="p">).</span><span class="nf">toBe</span><span class="p">(</span><span class="dl">"</span><span class="s2">text</span><span class="dl">"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre>
</div>
<p>This pattern makes each handler independently testable without spinning up the full server. Stub the downstream calls with your test framework&#8217;s mocking utilities. Cover the happy path, malformed input, and downstream failure.</p>
<p>Resource handlers work the same way. Extract, test in isolation, stub dependencies.</p>
<h2>
<p>  Contract assertions against the MCP schema<br />
</p></h2>
<p>After unit tests, the next layer checks that your tool registrations conform to the MCP protocol. A contract test instantiates the real server and sends actual protocol requests, then asserts on the response shape.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">Client</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@modelcontextprotocol/sdk/client/index.js</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">InMemoryTransport</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@modelcontextprotocol/sdk/inMemory.js</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">createServer</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./server</span><span class="dl">"</span><span class="p">;</span>

<span class="nf">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">list tools returns registered tools</span><span class="dl">"</span><span class="p">,</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">server</span> <span class="o">=</span> <span class="nf">createServer</span><span class="p">();</span>
  <span class="kd">const</span> <span class="p">[</span><span class="nx">clientTransport</span><span class="p">,</span> <span class="nx">serverTransport</span><span class="p">]</span> <span class="o">=</span> <span class="nx">InMemoryTransport</span><span class="p">.</span><span class="nf">createLinkedPair</span><span class="p">();</span>
  <span class="k">await</span> <span class="nx">server</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="nx">serverTransport</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">client</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Client</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">"</span><span class="s2">test</span><span class="dl">"</span><span class="p">,</span> <span class="na">version</span><span class="p">:</span> <span class="dl">"</span><span class="s2">1.0.0</span><span class="dl">"</span> <span class="p">},</span> <span class="p">{});</span>
  <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="nx">clientTransport</span><span class="p">);</span>

  <span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">client</span><span class="p">.</span><span class="nf">listTools</span><span class="p">();</span>
  <span class="kd">const</span> <span class="nx">toolNames</span> <span class="o">=</span> <span class="nx">result</span><span class="p">.</span><span class="nx">tools</span><span class="p">.</span><span class="nf">map</span><span class="p">((</span><span class="nx">t</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">t</span><span class="p">.</span><span class="nx">name</span><span class="p">);</span>
  <span class="nf">expect</span><span class="p">(</span><span class="nx">toolNames</span><span class="p">).</span><span class="nf">toContain</span><span class="p">(</span><span class="dl">"</span><span class="s2">get-item</span><span class="dl">"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre>
</div>
<p>The <code>InMemoryTransport</code> in the SDK is designed exactly for this. It lets you run client and server in the same process without any network, which keeps tests fast and deterministic.</p>
<p>Assert on the full response shape for each tool: input schema, output content types, error response format. This is the layer that catches the gap between what your server claims to support and what it actually returns.</p>
<h2>
<p>  Testing Streamable HTTP behavior<br />
</p></h2>
<p>The in memory transport covers the protocol layer. Testing the HTTP transport layer catches a different class of failure: auth middleware, session header handling, and the streaming path.</p>
<p>Stand up a real HTTP server on a random port, run your requests against it, and shut it down after each test.
</p>
<div class="highlight js-code-highlight">
<pre class="highlight typescript"><code><span class="k">import</span> <span class="p">{</span> <span class="nx">createServer</span> <span class="k">as</span> <span class="nx">createHttpServer</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">http</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">StreamableHTTPServerTransport</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">@modelcontextprotocol/sdk/server/streamableHttp.js</span><span class="dl">"</span><span class="p">;</span>

<span class="kd">let</span> <span class="nx">httpServer</span><span class="p">:</span> <span class="nb">ReturnType</span><span class="o">&lt;</span><span class="k">typeof</span> <span class="nx">createHttpServer</span><span class="o">&gt;</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">baseUrl</span><span class="p">:</span> <span class="kr">string</span><span class="p">;</span>

<span class="nf">beforeAll</span><span class="p">(</span><span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">transport</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">StreamableHTTPServerTransport</span><span class="p">({</span>
    <span class="na">sessionIdGenerator</span><span class="p">:</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nx">crypto</span><span class="p">.</span><span class="nf">randomUUID</span><span class="p">(),</span>
  <span class="p">});</span>
  <span class="kd">const</span> <span class="nx">mcpServer</span> <span class="o">=</span> <span class="nf">createServer</span><span class="p">();</span>
  <span class="k">await</span> <span class="nx">mcpServer</span><span class="p">.</span><span class="nf">connect</span><span class="p">(</span><span class="nx">transport</span><span class="p">);</span>
  <span class="nx">httpServer</span> <span class="o">=</span> <span class="nf">createHttpServer</span><span class="p">((</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">transport</span><span class="p">.</span><span class="nf">handleRequest</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">res</span><span class="p">));</span>
  <span class="k">await</span> <span class="k">new</span> <span class="nb">Promise</span><span class="o">&lt;</span><span class="k">void</span><span class="o">&gt;</span><span class="p">((</span><span class="nx">r</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">httpServer</span><span class="p">.</span><span class="nf">listen</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nf">r</span><span class="p">()));</span>
  <span class="kd">const</span> <span class="nx">addr</span> <span class="o">=</span> <span class="nx">httpServer</span><span class="p">.</span><span class="nf">address</span><span class="p">()</span> <span class="k">as</span> <span class="p">{</span> <span class="na">port</span><span class="p">:</span> <span class="kr">number</span> <span class="p">};</span>
  <span class="nx">baseUrl</span> <span class="o">=</span> <span class="s2">`http://localhost:</span><span class="p">${</span><span class="nx">addr</span><span class="p">.</span><span class="nx">port</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
<span class="p">});</span>

<span class="nf">afterAll</span><span class="p">(()</span> <span class="o">=&gt;</span> <span class="nx">httpServer</span><span class="p">.</span><span class="nf">close</span><span class="p">());</span>

<span class="nf">test</span><span class="p">(</span><span class="dl">"</span><span class="s2">POST returns a valid MCP response</span><span class="dl">"</span><span class="p">,</span> <span class="k">async </span><span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">fetch</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">baseUrl</span><span class="p">}</span><span class="s2">/mcp`</span><span class="p">,</span> <span class="p">{</span>
    <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span>
    <span class="na">headers</span><span class="p">:</span> <span class="p">{</span> <span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">application/json</span><span class="dl">"</span> <span class="p">},</span>
    <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="na">jsonrpc</span><span class="p">:</span> <span class="dl">"</span><span class="s2">2.0</span><span class="dl">"</span><span class="p">,</span> <span class="na">method</span><span class="p">:</span> <span class="dl">"</span><span class="s2">tools/list</span><span class="dl">"</span><span class="p">,</span> <span class="na">params</span><span class="p">:</span> <span class="p">{},</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span> <span class="p">}),</span>
  <span class="p">});</span>
  <span class="nf">expect</span><span class="p">(</span><span class="nx">res</span><span class="p">.</span><span class="nx">status</span><span class="p">).</span><span class="nf">toBe</span><span class="p">(</span><span class="mi">200</span><span class="p">);</span>
  <span class="kd">const</span> <span class="nx">json</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">res</span><span class="p">.</span><span class="nf">json</span><span class="p">();</span>
  <span class="nf">expect</span><span class="p">(</span><span class="nx">json</span><span class="p">.</span><span class="nx">result</span><span class="p">.</span><span class="nx">tools</span><span class="p">).</span><span class="nf">toBeDefined</span><span class="p">();</span>
<span class="p">});</span>
</code></pre>
</div>
<p>Add a test that opens GET on the same endpoint and confirms the SSE connection accepts. Add a test that sends an invalid session ID and checks the server handles it without crashing.</p>
<h2>
<p>  A CI setup that catches regressions<br />
</p></h2>
<p>A test suite only helps if it runs consistently. For MCP servers, the minimal CI setup is:</p>
<ul>
<li>Unit tests on every commit (fast, no network)</li>
<li>Contract tests via <code>InMemoryTransport</code> on every commit (still fast)</li>
<li>HTTP transport tests on pull requests and on merge to main</li>
</ul>
<p>If you deploy across multiple instances, add a test that starts two server processes and verifies that a session created on instance one can be resumed on instance two. This tests your external session store. It is slower, so running it on pull requests is reasonable.</p>
<p>If you are building an AI product that depends on your MCP server, the deployment and observability patterns in <a href="https://mudassirkhan.me/services/nextjs-for-ai-products" rel="noopener noreferrer">Next.js for AI products</a> apply directly to the production layer above the server. For the enterprise session model, see <a href="https://mudassirkhan.me/blog/mcp-enterprise-agents" rel="noopener noreferrer">MCP for enterprise agents</a>.</p>
<h2>
<p>  FAQ<br />
</p></h2>
<p><strong>What is an MCP server?</strong><br />
An MCP server exposes tools, resources, and prompts to LLM clients via the Model Context Protocol. Clients connect to it and call tools by name, receiving structured results.</p>
<p><strong>How do you test an MCP server?</strong><br />
Start with unit tests on each tool handler in isolation. Add contract tests using <code>InMemoryTransport</code>. Add HTTP transport tests against a local server instance for the full network path.</p>
<p><strong>What transport does MCP use?</strong><br />
MCP supports stdio for local use and Streamable HTTP for networked servers. Streamable HTTP uses a single endpoint that handles POST requests for tool calls and GET requests for SSE streaming. The TypeScript SDK supports Streamable HTTP from version 1.10.0 onward.</p>
<p><em>Building production AI systems? I consult on agentic AI and AI product engineering at <a href="https://mudassirkhan.me/" rel="noopener noreferrer">mudassirkhan.me</a>.</em></p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/how-to-test-mcp-servers-in-typescript-before-they-break-in-production/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>I built ffmpeg.download because installing FFmpeg shouldn&#8217;t take six decisions</title>
		<link>https://codango.com/i-built-ffmpeg-download-because-installing-ffmpeg-shouldnt-take-six-decisions/</link>
					<comments>https://codango.com/i-built-ffmpeg-download-because-installing-ffmpeg-shouldnt-take-six-decisions/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 10:14:09 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/i-built-ffmpeg-download-because-installing-ffmpeg-shouldnt-take-six-decisions/</guid>

					<description><![CDATA[I worked with FFmpeg on several large projects, which means I spent more time than is healthy answering &#8220;which FFmpeg do I install?&#8221; for myself and for other users. Installing <a class="more-link" href="https://codango.com/i-built-ffmpeg-download-because-installing-ffmpeg-shouldnt-take-six-decisions/">Continue reading <span class="screen-reader-text">  I built ffmpeg.download because installing FFmpeg shouldn&#8217;t take six decisions</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<p>I worked with FFmpeg on several large projects, which means I spent more time than is healthy answering &#8220;which FFmpeg do I install?&#8221; for myself and for other users.</p>
<p>Installing FFmpeg should be one decision: download. In practice, it&#8217;s a stack:</p>
<ol>
<li>Which build to pick? Or, should I build it myself?</li>
<li>Which license variant (GPL vs LGPL, with or without <code>--enable-nonfree</code>)</li>
<li>Which codecs and accelerators (x264, x265, NVENC, QSV, VA-API, libfdk_aac, AV1)</li>
<li>Shared or static</li>
<li>Which OS and arch (Windows x64, Linux x86_64/ARM64, Apple Silicon, musl)</li>
<li>Whether you can legally redistribute it inside your own app</li>
</ol>
<p>Most pages cover one slice and assume you already know the rest. So it turns into tab-hopping across release notes, comparing build matrices, and guessing whether the binary you grabbed will hit <code>nvenc not found</code> at runtime.</p>
<p>So I built <a href="https://www.ffmpeg.download/" rel="noopener noreferrer">ffmpeg.download</a>. Answer a few questions — OS, what you need to encode/decode, whether you&#8217;re shipping it inside something — get a direct link to a specific build with its checksum and source.</p>
<p>Cases I built it for first:</p>
<ol>
<li>
<code>ffmpeg: command not found</code> on a server — static Linux build, no apt/yum dance</li>
<li>NVENC on Windows — a build that actually has the encoder compiled in</li>
<li>Embedding in a commercial app — LGPL build, redistribution-safe</li>
<li>Apple Silicon native — not Rosetta, not a generic universal binary</li>
<li>Reproducible CI — pinned versions with checksums</li>
<li>Managing your production enviroment with consistent ffmpeg versions</li>
</ol>
<p>It&#8217;s a simple-to-use index of the trusted providers, filtered by what you actually need.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/i-built-ffmpeg-download-because-installing-ffmpeg-shouldnt-take-six-decisions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Designing Healthcare Interoperability with HL7 FHIR: Lessons from Venezuela</title>
		<link>https://codango.com/designing-healthcare-interoperability-with-hl7-fhir-lessons-from-venezuela/</link>
					<comments>https://codango.com/designing-healthcare-interoperability-with-hl7-fhir-lessons-from-venezuela/#respond</comments>
		
		<dc:creator><![CDATA[Codango Admin]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 10:09:36 +0000</pubDate>
				<category><![CDATA[Codango® Blog]]></category>
		<guid isPermaLink="false">https://codango.com/designing-healthcare-interoperability-with-hl7-fhir-lessons-from-venezuela/</guid>

					<description><![CDATA[How to move from isolated hospital systems to event‑driven, FHIR‑based architectures in a context with poor connectivity and legacy vendors. In many private clinics in Venezuela, “digital transformation” has meant <a class="more-link" href="https://codango.com/designing-healthcare-interoperability-with-hl7-fhir-lessons-from-venezuela/">Continue reading <span class="screen-reader-text">  Designing Healthcare Interoperability with HL7 FHIR: Lessons from Venezuela</span><span class="meta-nav">&#8594;</span></a>]]></description>
										<content:encoded><![CDATA[<h2>
<p>  How to move from isolated hospital systems to event‑driven, FHIR‑based architectures in a context with poor connectivity and legacy vendors.<br />
</p></h2>
<p>In many private clinics in Venezuela, “digital transformation” has meant installing a Hospital Information System (HIS) and maybe an Electronic Health Record (EHR). The problem is that most of these systems do not expose standard APIs, or do it in a very limited way.</p>
<p>As an engineer, the real challenge is not “going paperless”, but making systems talk to each other: getting lab, pharmacy, admissions, telemedicine and billing to exchange data without adding more technical debt.</p>
<p>In this post I’ll share a practical architectural approach, from a developer’s perspective, to move a typical clinic environment from ad‑hoc integrations to an HL7 FHIR + event‑driven architecture.</p>
<p>The starting point: interoperability level 1–2<br />
In practice, most clinics I work with operate like this:</p>
<p>Manual CSV or PDF exports.</p>
<p>Occasional point‑to‑point HL7 v2 interfaces (if you’re lucky).</p>
<p>No formal API documentation.</p>
<p>Human “copy‑paste” processes between systems.</p>
<p>In HIMSS terms, that’s level 1–2 interoperability. A realistic technical goal is to bring critical systems up to level 3 (semantic), where data means the same thing everywhere.</p>
<p><strong>Why FHIR fits this environment</strong><br />
FHIR brings several properties that are extremely valuable in this context:</p>
<p><strong>API‑first</strong>. Everything revolves around HTTP resources.</p>
<p><strong>Well‑known structures</strong>. JSON with clear fields for Patient, Observation, Encounter, Medication, etc.</p>
<p><strong>Mature implementations</strong>. HAPI FHIR in Java, for example, lets you spin up a server quickly.</p>
<p>A minimal viable pattern looks like this:</p>
<p>Deploy a FHIR server (e.g., HAPI FHIR) as the central point.</p>
<p>Expose a small, focused subset of resources for the first use case: Patient, Encounter, Observation, Medication, DiagnosticReport.</p>
<p>Build one adapter per legacy system that:</p>
<p>Consumes events or exports from the source system.</p>
<p>Transforms them into the corresponding FHIR resource.</p>
<p>Publishes them to the FHIR server via its REST API.</p>
<p>Integration layer: avoiding point‑to‑point hell<br />
Without an integration layer, every integration is a fixed pair of systems. With ten systems, you have forty‑five possible pairs.</p>
<p>Recommended architecture:</p>
<p>An <strong>event bus</strong> (Apache Kafka for high‑volume scenarios, RabbitMQ for more modest loads).</p>
<p>Microservices (Java + Spring Boot, for example) responsible for:</p>
<p>Subscribing to domain‑specific topics (lab, pharmacy, admissions, etc.).</p>
<p>Transforming messages to the FHIR model.</p>
<p>Publishing to the FHIR server.</p>
<p>In parallel, an integration engine like Mirth Connect can simplify HL7 v2 → FHIR handling.</p>
<p>This shifts the problem from “every system talks to every other system” to “every system talks to the bus”, and “the bus talks FHIR to the outside world”.</p>
<p>Dealing with unreliable connectivity<br />
In Venezuela, you simply cannot assume stable connectivity, not even within the same city. From a software architecture standpoint, this means:</p>
<p>Implementing the Outbox Pattern in source systems.</p>
<p>Using local queues to buffer events when the connection is down, and flush them when it comes back.</p>
<p>Designing idempotent consumers on the event bus side to avoid duplicates and inconsistencies.</p>
<p>From this perspective, the FHIR server becomes the consolidated source of truth, but local systems must still be able to operate with a subset of data while offline.</p>
<p>Security by design, not as an afterthought<br />
<strong>Two concrete recommendations</strong>:</p>
<ul>
<li>For client applications (web, mobile, clinical front‑end): SMART on FHIR on top of OAuth 2.0, with fine‑grained scopes per resource.</li>
<li>For server‑to‑server integrations: OAuth 2.0 with trusted clients, managed certificates and TLS 1.3 everywhere.</li>
</ul>
<p>Trying to “bolt on” security at the end is a terrible idea in a domain where data is, by definition, highly sensitive.</p>
<p>Conclusion and next steps<br />
If you’re in a similar environment (private clinic in LATAM with multiple legacy systems) and want to start working with FHIR, my suggestion is:</p>
<p>Pick one small but critical use case (for instance, integrating lab results into the EHR).</p>
<p>Stand up a FHIR server with a minimal resource model.</p>
<p>Build a single adapter for one source system.</p>
<p>Add the event bus and security from day one, even if you only have a couple of systems.</p>
<p>From there, adding new systems becomes a repeatable pattern, not a heroic project every single time.</p>
<p>Call to action for dev.to<br />
If you’re working on healthcare interoperability in LATAM and want to compare architectures or trade war stories, I’d be happy to connect. You can find more details and case studies at <a href="https://codebymelendez.com/" rel="noopener noreferrer">codebymelendez.com</a> or reach out to me on LinkedIn.</p>]]></content:encoded>
					
					<wfw:commentRss>https://codango.com/designing-healthcare-interoperability-with-hl7-fhir-lessons-from-venezuela/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
