<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>felix hernandez vieyra</title>
    <link>https://felix-hzv.dev/blog</link>
    <description>thoughts on software engineering, web development, and technology.</description>
    <language>en</language>
    <atom:link href="https://felix-hzv.dev/rss.xml" rel="self" type="application/rss+xml" />
    <lastBuildDate>Sun, 19 Apr 2026 06:45:58 GMT</lastBuildDate>
    <item>
      <title>&quot;are we there yet?&quot;, said the client to the server</title>
      <link>https://felix-hzv.dev/blog/are-we-there-yet</link>
      <guid isPermaLink="true">https://felix-hzv.dev/blog/are-we-there-yet</guid>
      <pubDate>Sun, 19 Apr 2026 06:45:58 GMT</pubDate>
      <description>5 best ways to teach your front-end some patience</description>
      <content:encoded><![CDATA[<p class="drop-cap">I recently needed my front-end to know when a data export was done. It's a simple <em>csv</em> or <em>xlsx</em> file, and unless the user has heaps of data, it takes a few seconds. The server just has to tell the client &quot;your file's ready&quot;. No progress bar, no cancel button.</p><p>Out of bad habit, my first thought was a status endpoint the client could poll (known in the hood as <strong>short-polling</strong>). I've built countless of these, and they seem to be the favourite way for dev teams to rack up tech debt they never pay off.</p><p>There are a few well-known options here, and it's mostly about picking the right one for your case. And if you ask what’s the purpose then, it’s because every time I do one of these light-hearted write-ups, I end up in deep and fantastic conversations with fellow nerds.</p><p>So, onward. Short-polling takes a minute to set up: you ask the job, on an interval, whether it's finished. The server answers, the connection closes, and you repeat until it fails or succeeds.</p><p>It's the developer's version of &quot;<strong>Are we there yet?</strong>&quot;. Most of those requests are worthless. They burn server cycles and bandwidth, and open the door to state management bugs you didn't need.</p><p>Interval is basically impossible to get right - too slow and the updates feel choppy, too eager and you're bloating the network. That said, for long-running jobs where nobody needs an instant update, a well-spaced interval is perfectly fine.</p><p>Long-polling is the same idea, slightly more sophisticated. Plain HTTP but the server holds the connection open until there's an actual update or it times out after ~30s.</p><p>Handling reconnections gracefully and timing them with a margin before the timeout can be a bit of a pain. Keeps threads captive on synchronous servers, which isn't ideal. Still, unless you have high-frequency updates, I'd pick it over short-polling. It has served devs well for a long time.</p><p>Now, the real deal: <strong>WebSockets</strong>. They start life as an HTTP request then upgrade to their own protocol over TCP. That's why proxies and firewall configs can be a bit of a <em>PIA</em> (pardon my french), but in return you get proper bidirectional streaming.</p><p>Either side sends messages whenever it wants, with only a few bytes of framing on each one. It's quick, but trade-offs are extra complexity on the infrastructure side.</p><p>Unless you really need low-latency, two-way streaming, think collaboration tools or multiplayer anything, they're overkill.</p><p>For my simple stuff, I went with <strong>Server-Sent Events</strong>. Same push-from-the-server idea, way easier to set up, and no load balancer drama.</p><p>It's still plain HTTP. The client opens a connection with the <code>EventSource API</code>, and the server streams text events the moment they happen. The browser handles reconnection for you and keeps track of the last event it received.</p><p>On the server, you return a streaming response with the <code>Content-Type</code> set to text/event-stream, and send each event as your generator yields it. The client gets them in real time instead of waiting for the function to finish.</p><figure class="code-block"><figcaption class="meta flex justify-between gap-4 px-5 md:px-6 pt-3"><span>python</span><span>7 lines</span></figcaption><pre tabindex="0" class="overflow-x-auto px-5 md:px-6 pt-3 pb-5 font-mono text-code text-ink"><code><span class="line"><span style="color:var(--shiki-token-function)">@app</span><span style="color:var(--shiki-token-punctuation)">.</span><span style="color:var(--shiki-token-function)">get</span><span style="color:var(--shiki-token-punctuation)">(</span><span style="color:var(--shiki-token-string-expression)">"/exports/</span><span style="color:var(--shiki-token-constant)">{job_id}</span><span style="color:var(--shiki-token-string-expression)">/events"</span><span style="color:var(--shiki-token-punctuation)">)</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">async</span><span style="color:var(--shiki-token-keyword)"> def</span><span style="color:var(--shiki-token-function)"> export_events</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-parameter)">job_id</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-constant)"> str</span><span style="color:var(--shiki-foreground)">):</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    async</span><span style="color:var(--shiki-token-keyword)"> def</span><span style="color:var(--shiki-token-function)"> stream</span><span style="color:var(--shiki-foreground)">():</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">        async</span><span style="color:var(--shiki-token-keyword)"> for</span><span style="color:var(--shiki-foreground)"> status </span><span style="color:var(--shiki-token-keyword)">in</span><span style="color:var(--shiki-token-function)"> watch_export</span><span style="color:var(--shiki-token-punctuation)">(job_id):</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">            yield</span><span style="color:var(--shiki-token-keyword)"> f</span><span style="color:var(--shiki-token-string-expression)">"id: </span><span style="color:var(--shiki-token-constant)">{</span><span style="color:var(--shiki-foreground)">status</span><span style="color:var(--shiki-token-punctuation)">.</span><span style="color:var(--shiki-foreground)">seq</span><span style="color:var(--shiki-token-constant)">}</span><span style="color:var(--shiki-foreground)">\n</span><span style="color:var(--shiki-token-string-expression)">event: status</span><span style="color:var(--shiki-foreground)">\n</span><span style="color:var(--shiki-token-string-expression)">data: </span><span style="color:var(--shiki-token-constant)">{</span><span style="color:var(--shiki-foreground)">status</span><span style="color:var(--shiki-token-punctuation)">.</span><span style="color:var(--shiki-token-function)">json</span><span style="color:var(--shiki-token-punctuation)">()</span><span style="color:var(--shiki-token-constant)">}</span><span style="color:var(--shiki-foreground)">\n\n</span><span style="color:var(--shiki-token-string-expression)">"</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    return</span><span style="color:var(--shiki-token-function)"> StreamingResponse</span><span style="color:var(--shiki-token-punctuation)">(</span><span style="color:var(--shiki-token-function)">stream</span><span style="color:var(--shiki-token-punctuation)">(), media_type</span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-string-expression)">"text/event-stream"</span><span style="color:var(--shiki-token-punctuation)">)</span></span></code></pre></figure><p>Then…</p><figure class="code-block"><figcaption class="meta flex justify-between gap-4 px-5 md:px-6 pt-3"><span>javascript</span><span>6 lines</span></figcaption><pre tabindex="0" class="overflow-x-auto px-5 md:px-6 pt-3 pb-5 font-mono text-code text-ink"><code><span class="line"><span style="color:var(--shiki-token-keyword)">const</span><span style="color:var(--shiki-token-constant)"> events</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-keyword)"> new</span><span style="color:var(--shiki-token-function)"> EventSource</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">`/exports/</span><span style="color:var(--shiki-token-keyword)">${</span><span style="color:var(--shiki-foreground)">jobId</span><span style="color:var(--shiki-token-keyword)">}</span><span style="color:var(--shiki-token-string-expression)">/events`</span><span style="color:var(--shiki-foreground)">);</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-token-constant)">events</span><span style="color:var(--shiki-token-function)">.addEventListener</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-string-expression)">"status"</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-foreground)"> (e) </span><span style="color:var(--shiki-token-keyword)">=></span><span style="color:var(--shiki-foreground)"> {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  const</span><span style="color:var(--shiki-token-constant)"> status</span><span style="color:var(--shiki-token-keyword)"> =</span><span style="color:var(--shiki-token-constant)"> JSON</span><span style="color:var(--shiki-token-function)">.parse</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-constant)">e</span><span style="color:var(--shiki-foreground)">.data);</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">  if</span><span style="color:var(--shiki-foreground)"> (</span><span style="color:var(--shiki-token-constant)">status</span><span style="color:var(--shiki-foreground)">.done) </span><span style="color:var(--shiki-token-constant)">events</span><span style="color:var(--shiki-token-function)">.close</span><span style="color:var(--shiki-foreground)">();</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">});</span></span></code></pre></figure><p>If you're on a Python back-end, it's dead simple, as most frameworks have you covered out of the box. On <strong>FastAPI</strong>, look at <code>StreamingResponse</code> (thx <strong>Starlette</strong>), on <strong>Flask</strong>, <code>stream_with_context</code>, and there are plenty of others.</p><p>My events are just plain text with data, event and id fields. The id is what makes reconnection easy: the browser sends it back as <code>Last-Event-ID</code> when it reconnects, so the server can resume from where it left off.</p><p>Know your options, you could open an envelope with a chainsaw too but it's prlly not the most efficient way to do it.</p>]]></content:encoded>
      <category>server-sent-events</category>
      <category>polling</category>
      <category>websockets</category>
      <category>design-patterns</category>
    </item>
    <item>
      <title>do list comprehensions dream of type safety?</title>
      <link>https://felix-hzv.dev/blog/rust-befriends-python</link>
      <guid isPermaLink="true">https://felix-hzv.dev/blog/rust-befriends-python</guid>
      <pubDate>Sat, 28 Mar 2026 21:54:36 GMT</pubDate>
      <description>sneaking a crab into my Python backend</description>
      <content:encoded><![CDATA[<p class="drop-cap">There's a class of problem in Python that no amount of async or clever caching will fix: CPU-bound work. Your server isn't waiting on a database or an API, it's just grinding through data, row by row.</p><p>I've hit this a couple of times building data exports on Python microservices. On my pet project, users can export their financial data as CSV, Excel or PDF, which means pulling transactions from the db and serialising potentially thousands of rows into formatted files.</p><p>&quot;Potentially&quot; is doing a lot of work in that sentence, because my DAU sits at ~2: girlfriend, myself, stop counting. We are still <em>far</em>, boys. It was just the perfect excuse to finally get hands-on with Rust bindings after reading about them for a while.</p><p><strong>TL;DR</strong> I wrote the serialisation layer in Rust, and I was surprised by how incredibly simple it was.</p><p>The magic is <code>PyO3</code>, a crate that lets you write native Python modules in Rust, paired with <code>maturin</code>, a build tool that compiles that Rust into a Python wheel. For a language with such a scary reputation, the setup is tiny: a <code>Cargo.toml</code> that pulls in <code>PyO3</code> with the extension-module feature, and a <code>pyproject.toml</code> that tells Python to use <code>maturin</code> as its build backend. Crab bindings… sorted.</p><p>On the Rust side, you annotate functions with <code>#[pyfunction]</code> and register them in a <code>#[pymodule]</code>. You pull values out of Python dicts, work with native Rust types inside, and hand back bytes. <code>PyO3</code> does the conversions for you.</p><figure class="code-block"><figcaption class="meta flex justify-between gap-4 px-5 md:px-6 pt-3"><span>rust</span><span>15 lines</span></figcaption><pre tabindex="0" class="overflow-x-auto px-5 md:px-6 pt-3 pb-5 font-mono text-code text-ink"><code><span class="line"><span style="color:var(--shiki-foreground)">#[pyfunction]</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">fn</span><span style="color:var(--shiki-token-function)"> export_csv</span><span style="color:var(--shiki-foreground)">&#x3C;'</span><span style="color:var(--shiki-token-function)">py</span><span style="color:var(--shiki-foreground)">>(</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    py</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> Python</span><span style="color:var(--shiki-foreground)">&#x3C;'</span><span style="color:var(--shiki-token-function)">py</span><span style="color:var(--shiki-foreground)">>,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    rows</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-function)"> Vec</span><span style="color:var(--shiki-foreground)">&#x3C;</span><span style="color:var(--shiki-token-function)">HashMap</span><span style="color:var(--shiki-foreground)">&#x3C;</span><span style="color:var(--shiki-token-function)">String</span><span style="color:var(--shiki-foreground)">, </span><span style="color:var(--shiki-token-function)">String</span><span style="color:var(--shiki-foreground)">>>,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    currency</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-keyword)"> &#x26;</span><span style="color:var(--shiki-token-function)">str</span><span style="color:var(--shiki-foreground)">,</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">) </span><span style="color:var(--shiki-token-keyword)">-></span><span style="color:var(--shiki-token-function)"> PyResult</span><span style="color:var(--shiki-foreground)">&#x3C;</span><span style="color:var(--shiki-token-function)">Bound</span><span style="color:var(--shiki-foreground)">&#x3C;'</span><span style="color:var(--shiki-token-function)">py</span><span style="color:var(--shiki-foreground)">, </span><span style="color:var(--shiki-token-function)">PyBytes</span><span style="color:var(--shiki-foreground)">>> {</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    let</span><span style="color:var(--shiki-foreground)"> csv </span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-token-function)"> build_csv</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-keyword)">&#x26;</span><span style="color:var(--shiki-foreground)">rows, currency)</span><span style="color:var(--shiki-token-keyword)">?</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">    Ok</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-function)">PyBytes</span><span style="color:var(--shiki-token-keyword)">::</span><span style="color:var(--shiki-token-function)">new</span><span style="color:var(--shiki-foreground)">(py, </span><span style="color:var(--shiki-token-keyword)">&#x26;</span><span style="color:var(--shiki-foreground)">csv))</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-foreground)">#[pymodule]</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">fn</span><span style="color:var(--shiki-token-function)"> scribe</span><span style="color:var(--shiki-foreground)">(m</span><span style="color:var(--shiki-token-keyword)">:</span><span style="color:var(--shiki-token-keyword)"> &#x26;</span><span style="color:var(--shiki-token-function)">Bound</span><span style="color:var(--shiki-foreground)">&#x3C;'</span><span style="color:var(--shiki-token-function)">_</span><span style="color:var(--shiki-foreground)">, </span><span style="color:var(--shiki-token-function)">PyModule</span><span style="color:var(--shiki-foreground)">>) </span><span style="color:var(--shiki-token-keyword)">-></span><span style="color:var(--shiki-token-function)"> PyResult</span><span style="color:var(--shiki-foreground)">&#x3C;()> {</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">    m</span><span style="color:var(--shiki-token-keyword)">.</span><span style="color:var(--shiki-token-function)">add_function</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-function)">wrap_pyfunction!</span><span style="color:var(--shiki-foreground)">(export_csv, m)</span><span style="color:var(--shiki-token-keyword)">?</span><span style="color:var(--shiki-foreground)">)</span><span style="color:var(--shiki-token-keyword)">?</span><span style="color:var(--shiki-foreground)">;</span></span>
<span class="line"><span style="color:var(--shiki-token-function)">    Ok</span><span style="color:var(--shiki-foreground)">(())</span></span>
<span class="line"><span style="color:var(--shiki-foreground)">}</span></span></code></pre></figure><p>From Python, the module is invisible. You import scribe, call <code>scribe.export_csv(rows, currency)</code>, and get bytes back. It's just a function call, and the rest of the codebase has no idea there's a crab in there.</p><figure class="code-block"><figcaption class="meta flex justify-between gap-4 px-5 md:px-6 pt-3"><span>python</span><span>3 lines</span></figcaption><pre tabindex="0" class="overflow-x-auto px-5 md:px-6 pt-3 pb-5 font-mono text-code text-ink"><code><span class="line"><span style="color:var(--shiki-token-keyword)">import</span><span style="color:var(--shiki-foreground)"> scribe</span></span>
<span class="line"></span>
<span class="line"><span style="color:var(--shiki-foreground)">payload </span><span style="color:var(--shiki-token-keyword)">=</span><span style="color:var(--shiki-foreground)"> scribe</span><span style="color:var(--shiki-token-punctuation)">.</span><span style="color:var(--shiki-token-function)">export_csv</span><span style="color:var(--shiki-token-punctuation)">(rows, currency)</span><span style="color:var(--shiki-token-comment)">  # plain bytes</span></span></code></pre></figure><p>Kept the boundary clean on purpose. Python does what it's good at: auth, db queries, request routing. It collects the data, turns db records into plain dicts, and hands them to the crate for the compute-heavy part.</p><p>The performance difference is real, but the bigger win is having Rust's type system and memory safety wrapped around the serialisation logic. A compiler that enforces exhaustive pattern matching on export scopes, and refuses to build when I forget one is worth the setup.</p><p>The elephant in the room is that writing Rust and actually understanding Rust are two very different worlds. Couple of obscure concepts to us, the uninitiated.</p><p>If you ask me, this is one of the best uses of developing in the age of Artificial Intelligence. I've got a wealth of knowledge on tap, and I can ask why the borrow checker hates me as many times as I need.</p>]]></content:encoded>
      <category>PyO3</category>
      <category>maturin</category>
      <category>Rust</category>
      <category>Python</category>
    </item>
    <item>
      <title>a developer&apos;s sleight of hand</title>
      <link>https://felix-hzv.dev/blog/a-developer-sleight-of-hand</link>
      <guid isPermaLink="true">https://felix-hzv.dev/blog/a-developer-sleight-of-hand</guid>
      <pubDate>Fri, 27 Mar 2026 23:07:00 GMT</pubDate>
      <description>swapping email providers in under an hour</description>
      <content:encoded><![CDATA[<p class="drop-cap">There's a very specific kind of joy when an abstraction you spent a few extra minutes on finally pays for itself. Mine showed up right before the weekend, which made it even better.</p><p>I swapped my whole email provider from AWS SES to Resend in under an hour. The provider change itself was about 20 lines of code, because everything email-related sits behind a small <code>EmailProvider</code> protocol.</p><p>Verification emails, welcome emails, suppression checks: all of it talks to that interface, and none of it knows what's behind it. So I wrote a <code>ResendProvider</code> that exposes the same methods on top of a different SDK, deleted the <code>SESProvider</code>, and rewrote the webhook handler for Resend's payload format.</p><p>Then I got to delete a good chunk of Terraform I no longer needed, which is my favourite kind of commit. Templates, rate limiting, the suppression list and token generation didn't change at all. Zero modifications to business logic.</p><p>The pattern is simple, and very tempting to break. You define a protocol for what your application needs from the dependency, not what the dependency happens to offer. Send an email. Check the suppression list. Handle a bounce.</p><figure class="code-block"><figcaption class="meta flex justify-between gap-4 px-5 md:px-6 pt-3"><span>python</span><span>4 lines</span></figcaption><pre tabindex="0" class="overflow-x-auto px-5 md:px-6 pt-3 pb-5 font-mono text-code text-ink"><code><span class="line"><span style="color:var(--shiki-token-keyword)">class</span><span style="color:var(--shiki-token-function)"> EmailProvider</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-function)">Protocol</span><span style="color:var(--shiki-foreground)">):</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    def</span><span style="color:var(--shiki-token-function)"> send</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-parameter)">self</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-parameter)"> message</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-foreground)"> EmailMessage) </span><span style="color:var(--shiki-token-punctuation)">-></span><span style="color:var(--shiki-token-constant)"> None</span><span style="color:var(--shiki-foreground)">: ...</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    def</span><span style="color:var(--shiki-token-function)"> is_suppressed</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-parameter)">self</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-parameter)"> address</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-constant)"> str</span><span style="color:var(--shiki-foreground)">) </span><span style="color:var(--shiki-token-punctuation)">-></span><span style="color:var(--shiki-token-constant)"> bool</span><span style="color:var(--shiki-foreground)">: ...</span></span>
<span class="line"><span style="color:var(--shiki-token-keyword)">    def</span><span style="color:var(--shiki-token-function)"> handle_bounce</span><span style="color:var(--shiki-foreground)">(</span><span style="color:var(--shiki-token-parameter)">self</span><span style="color:var(--shiki-token-punctuation)">,</span><span style="color:var(--shiki-token-parameter)"> payload</span><span style="color:var(--shiki-token-punctuation)">:</span><span style="color:var(--shiki-token-constant)"> dict</span><span style="color:var(--shiki-foreground)">) </span><span style="color:var(--shiki-token-punctuation)">-></span><span style="color:var(--shiki-token-constant)"> None</span><span style="color:var(--shiki-foreground)">: ...</span></span></code></pre></figure><p>Then you write a thin adapter that maps the provider's SDK onto that protocol, and the rest of the app only ever imports the protocol.</p><p>The hard part is discipline. Keep the adapter thin, forever. No business logic, no special cases, no magic. The moment decisions creep into the adapter, you're coupled to the provider again, just with extra steps.</p><p>This goes well beyond email: payment processors, notification services, storage backends, anything where a third party owns the implementation. If swapping it means writing one new class and deleting the old one, you did the work right.</p>]]></content:encoded>
      <category>design-patterns</category>
      <category>aws-ses</category>
      <category>resend</category>
    </item>
  </channel>
</rss>
