<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Martin Palopoli</title>
    <description>The latest articles on DEV Community by Martin Palopoli (@martin_palopoli).</description>
    <link>https://dev.to/martin_palopoli</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1593262%2F1f1a00c6-c7c8-4524-9741-0cc1ae77f741.JPG</url>
      <title>DEV Community: Martin Palopoli</title>
      <link>https://dev.to/martin_palopoli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/martin_palopoli"/>
    <language>en</language>
    <item>
      <title>Forms, payloads, and live inputs in Fitz LiveViews</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:51:12 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/forms-payloads-and-live-inputs-in-fitz-liveviews-42od</link>
      <guid>https://dev.to/martin_palopoli/forms-payloads-and-live-inputs-in-fitz-liveviews-42od</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Events in &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;Fitz LiveViews&lt;/a&gt; carry data three ways: a &lt;strong&gt;click payload&lt;/strong&gt; (&lt;code&gt;data-flv-value-*&lt;/code&gt;) tags a button with the value it should send; a &lt;strong&gt;form submit&lt;/strong&gt; (&lt;code&gt;data-flv-submit&lt;/code&gt;) reads the form's named inputs; and a &lt;strong&gt;live value&lt;/strong&gt; (&lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt;) delivers a control's current value in &lt;code&gt;payload["value"]&lt;/code&gt;. All three land in the same place — a &lt;code&gt;payload&lt;/code&gt; map your handler reads. This post builds a live name list (add / remove / count) that runs both server-rendered and as WebAssembly. &lt;em&gt;(Part 3 of the FitzLiveViews series.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Parts &lt;a href="https://dev.to/"&gt;1&lt;/a&gt; and &lt;a href="https://dev.to/"&gt;2&lt;/a&gt; covered the pitch and the counter. A counter only reads &lt;code&gt;+1&lt;/code&gt;/&lt;code&gt;-1&lt;/code&gt; — no data flows &lt;em&gt;in&lt;/em&gt;. Real UIs take input: text, selections, form fields. Here's how that data reaches your handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;payload&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Every event handler has a &lt;code&gt;payload&lt;/code&gt; in scope — a &lt;code&gt;Map&amp;lt;Str, Str&amp;gt;&lt;/code&gt;. The three mechanisms below all fill it; your handler reads it with &lt;code&gt;payload["key"]&lt;/code&gt; (guard with &lt;code&gt;payload.has("key")&lt;/code&gt;):&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Click payload — a button that carries a value
&lt;/h3&gt;

&lt;p&gt;Tag any element with &lt;code&gt;data-flv-value-&amp;lt;key&amp;gt;="{expr}"&lt;/code&gt;, and when a &lt;code&gt;data-flv-click&lt;/code&gt; on it (or an ancestor) fires, that value rides along:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-flv-click=&lt;/span&gt;&lt;span class="s"&gt;"remove"&lt;/span&gt; &lt;span class="na"&gt;data-flv-value-item=&lt;/span&gt;&lt;span class="s"&gt;"{it}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;×&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The delete button &lt;em&gt;knows which row it is&lt;/em&gt; because the row's value is stamped on it. No IDs threaded through a callback, no closure capture.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Form submit — the whole form at once
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;data-flv-submit="handler"&lt;/code&gt; on a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; reads each named input into the payload on submit; &lt;code&gt;data-flv-clear&lt;/code&gt; resets a field afterward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;data-flv-submit=&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Add a name"&lt;/span&gt; &lt;span class="na"&gt;data-flv-clear&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;payload["item"]&lt;/code&gt; is the input's value at submit time. No &lt;code&gt;preventDefault&lt;/code&gt;, no &lt;code&gt;FormData&lt;/code&gt;, no &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Live value — &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;For a control that reports as you type or on selection, &lt;code&gt;@input&lt;/code&gt; (every keystroke) and &lt;code&gt;@change&lt;/code&gt; (on blur/selection) deliver the current value in &lt;code&gt;payload["value"]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;input=&lt;/span&gt;&lt;span class="s"&gt;"on_name"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{name}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;change=&lt;/span&gt;&lt;span class="s"&gt;"on_color"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; … &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;on_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same &lt;code&gt;payload["value"]&lt;/code&gt; covers &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;, and &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;. On the server-rendered target this is the classic &lt;code&gt;data-flv-change&lt;/code&gt; attribute; on the client-WASM target it's the &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt; decorator — same payload, same handler, so one &lt;code&gt;.fitzv&lt;/code&gt; serves both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together — a live name list
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;component NameList {
  state {
    names: List&lt;span class="nt"&gt;&amp;lt;Str&amp;gt;&lt;/span&gt; = ["Ada", "Grace", "Margaret"]
  }

  event add() {
    if (payload.has("item")) {
      let n = payload["item"]
      if (n != "") { names.push(n) }
    }
  }

  event remove() {
    if (payload.has("item")) {
      let target = payload["item"]
      names = names.filter(fn(it) =&amp;gt; it != target)
    }
  }

  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"names"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;data-flv-submit=&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Add a name"&lt;/span&gt; &lt;span class="na"&gt;data-flv-clear&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
        {#for it in names}
          &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;{it} &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-flv-click=&lt;/span&gt;&lt;span class="s"&gt;"remove"&lt;/span&gt; &lt;span class="na"&gt;data-flv-value-item=&lt;/span&gt;&lt;span class="s"&gt;"{it}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;×&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
        {/for}
      &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;{names.len()} total&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{#for it in names}&lt;/code&gt; iterates; &lt;code&gt;{it}&lt;/code&gt; interpolates each item; the remove button stamps its own value. Add a name, remove a row, watch the count — no client code, no API.&lt;/p&gt;

&lt;p&gt;Wire it into a &lt;code&gt;main.fitz&lt;/code&gt; (the same shape as the counter in &lt;a href="https://dev.to/"&gt;part 2&lt;/a&gt;) and it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flv_register&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;NameList&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NameList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_remove&lt;/span&gt;

&lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;names-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WsConn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
  &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fitz run&lt;/code&gt;, open &lt;code&gt;http://localhost:3000&lt;/code&gt;, add and remove names. (I ran exactly this — the page renders the seeded list, the count, and the wired form/buttons.) The &lt;strong&gt;client-WASM build of the same list&lt;/strong&gt; is running in the &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/embed/?c=namelist" rel="noopener noreferrer"&gt;live gallery&lt;/a&gt; — add/remove/count entirely in your browser, offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two honest caveats
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live text inputs re-mount.&lt;/strong&gt; The current rendering model is dirty-flag + naive re-render: a state change rebuilds the component's DOM. For a &lt;code&gt;&amp;lt;select&amp;gt; @change&lt;/code&gt; that's invisible; for a live text &lt;code&gt;&amp;lt;input&amp;gt; @input&lt;/code&gt;, the field re-mounts each keystroke — the value re-binds via &lt;code&gt;value="{name}"&lt;/code&gt;, but the caret jumps to the end. Fine-grained reactivity (patch in place) is the next rendering-model step, and it's what fixes this. The value always reaches the handler reliably; that's what &lt;code&gt;@input&lt;/code&gt; guarantees today.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;String methods on WASM — now supported.&lt;/strong&gt; A case-insensitive filter (&lt;code&gt;names.filter(fn(x) =&amp;gt; q == "" or x.lower().contains(q.lower()))&lt;/code&gt;) used to be server-rendered only; as of Fitz core v0.29.8 the string methods (&lt;code&gt;.lower&lt;/code&gt; / &lt;code&gt;.contains&lt;/code&gt; / …) and logical &lt;code&gt;and&lt;/code&gt;/&lt;code&gt;or&lt;/code&gt; compile on the client-WASM target too, so the filter runs offline. The only remaining wrinkle is the caret caveat above for the &lt;em&gt;filter box itself&lt;/em&gt; while you type.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next in this series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#4+ — Building the flagship.&lt;/strong&gt; A complete admin panel in Fitz + Fitz LiveViews: cookie-based auth (Argon2id + JWT), live DataGrids querying Postgres over WebSockets, the packaged UI library, i18n, and a one-command Docker setup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If typed events without a client framework sound good, star the &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt;. Next: a real app.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Forms, payloads e inputs en vivo en Fitz LiveViews</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:49:07 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/forms-payloads-e-inputs-en-vivo-en-fitz-liveviews-4fmi</link>
      <guid>https://dev.to/martin_palopoli/forms-payloads-e-inputs-en-vivo-en-fitz-liveviews-4fmi</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Los eventos en &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;Fitz LiveViews&lt;/a&gt; llevan data de tres formas: un &lt;strong&gt;click payload&lt;/strong&gt; (&lt;code&gt;data-flv-value-*&lt;/code&gt;) marca un botón con el valor que tiene que mandar; un &lt;strong&gt;form submit&lt;/strong&gt; (&lt;code&gt;data-flv-submit&lt;/code&gt;) lee los inputs con nombre del form; y un &lt;strong&gt;valor en vivo&lt;/strong&gt; (&lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt;) entrega el valor actual de un control en &lt;code&gt;payload["value"]&lt;/code&gt;. Las tres caen en el mismo lado — un mapa &lt;code&gt;payload&lt;/code&gt; que tu handler lee. Este post construye una lista de nombres en vivo (agregar / quitar / contar) que corre server-rendered y como WebAssembly. &lt;em&gt;(Parte 3 de la serie FitzLiveViews.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Las partes &lt;a href="https://dev.to/"&gt;1&lt;/a&gt; y &lt;a href="https://dev.to/"&gt;2&lt;/a&gt; cubrieron el pitch y el counter. Un counter solo lee &lt;code&gt;+1&lt;/code&gt;/&lt;code&gt;-1&lt;/code&gt; — no entra data. Las UIs reales toman input: texto, selecciones, campos de formulario. Acá está cómo esa data llega a tus handlers.&lt;/p&gt;

&lt;h2&gt;
  
  
  El &lt;code&gt;payload&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Cada handler de evento tiene un &lt;code&gt;payload&lt;/code&gt; en scope — un &lt;code&gt;Map&amp;lt;Str, Str&amp;gt;&lt;/code&gt;. Los tres mecanismos de abajo lo llenan; tu handler lo lee con &lt;code&gt;payload["clave"]&lt;/code&gt; (guardá con &lt;code&gt;payload.has("clave")&lt;/code&gt;):&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Click payload — un botón que lleva un valor
&lt;/h3&gt;

&lt;p&gt;Marcá cualquier elemento con &lt;code&gt;data-flv-value-&amp;lt;clave&amp;gt;="{expr}"&lt;/code&gt;, y cuando un &lt;code&gt;data-flv-click&lt;/code&gt; sobre él (o un ancestro) dispara, ese valor viaja:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-flv-click=&lt;/span&gt;&lt;span class="s"&gt;"remove"&lt;/span&gt; &lt;span class="na"&gt;data-flv-value-item=&lt;/span&gt;&lt;span class="s"&gt;"{it}"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;×&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El botón de borrar &lt;em&gt;sabe qué fila es&lt;/em&gt; porque el valor de la fila está estampado en él. Sin IDs pasados por un callback, sin captura de closure.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Form submit — el form entero de una
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;data-flv-submit="handler"&lt;/code&gt; en un &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; lee cada input con nombre al payload en el submit; &lt;code&gt;data-flv-clear&lt;/code&gt; limpia el campo después:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;data-flv-submit=&lt;/span&gt;&lt;span class="s"&gt;"add"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"item"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Agregar un nombre"&lt;/span&gt; &lt;span class="na"&gt;data-flv-clear&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Agregar&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;payload["item"]&lt;/code&gt; es el valor del input al momento del submit. Sin &lt;code&gt;preventDefault&lt;/code&gt;, sin &lt;code&gt;FormData&lt;/code&gt;, sin &lt;code&gt;fetch&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Valor en vivo — &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Para un control que reporta mientras tipeás o al seleccionar, &lt;code&gt;@input&lt;/code&gt; (cada tecla) y &lt;code&gt;@change&lt;/code&gt; (al blur/selección) entregan el valor actual en &lt;code&gt;payload["value"]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;input=&lt;/span&gt;&lt;span class="s"&gt;"on_name"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"{name}"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;change=&lt;/span&gt;&lt;span class="s"&gt;"on_color"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt; … &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;on_name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El mismo &lt;code&gt;payload["value"]&lt;/code&gt; cubre &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; y &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;. En el target server-rendered esto es el atributo clásico &lt;code&gt;data-flv-change&lt;/code&gt;; en el target client-WASM es el decorador &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt; — mismo payload, mismo handler, así que un &lt;code&gt;.fitzv&lt;/code&gt; sirve a los dos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Todo junto — una lista de nombres en vivo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;NameList&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;names&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ada&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Grace&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Margaret&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="nx"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;names&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;flv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;add&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;item&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;placeholder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Agregar un nombre&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;flv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;clear&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Agregar&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ul&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;it&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;flv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;remove&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;flv&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{it}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;×&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/ul&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/template&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{#for it in names}&lt;/code&gt; itera; &lt;code&gt;{it}&lt;/code&gt; interpola cada ítem; el botón de borrar estampa su propio valor. Agregá un nombre, quitá una fila, mirá el contador — sin código de cliente, sin API.&lt;/p&gt;

&lt;p&gt;Cablealo a un &lt;code&gt;main.fitz&lt;/code&gt; (la misma forma que el counter de la &lt;a href="https://dev.to/"&gt;parte 2&lt;/a&gt;) y corre:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flv_register&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;NameList&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NameList&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NameList_remove&lt;/span&gt;

&lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;names-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/names&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WsConn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
  &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;NameList&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;fitz run&lt;/code&gt;, abrís &lt;code&gt;http://localhost:3000&lt;/code&gt;, agregás y quitás nombres. (Corrí exactamente esto — la página renderiza la lista sembrada, el contador, y el form/botones cableados.) El &lt;strong&gt;build client-WASM de la misma lista&lt;/strong&gt; está corriendo en la &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/embed/?c=namelist" rel="noopener noreferrer"&gt;galería en vivo&lt;/a&gt; — agregar/quitar/contar entero en tu browser, offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dos caveats honestos
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Los inputs de texto en vivo se re-montan.&lt;/strong&gt; El modelo de render actual es dirty-flag + naive re-render: un cambio de estado reconstruye el DOM del componente. Para un &lt;code&gt;&amp;lt;select&amp;gt; @change&lt;/code&gt; es invisible; para un &lt;code&gt;&amp;lt;input&amp;gt; @input&lt;/code&gt; en vivo, el campo se re-monta en cada tecla — el valor se re-bindea con &lt;code&gt;value="{name}"&lt;/code&gt;, pero el caret salta al final. La reactividad fine-grained (patch in-place) es el próximo paso del modelo de render, y es lo que arregla esto. El valor siempre llega al handler de forma confiable; eso es lo que &lt;code&gt;@input&lt;/code&gt; garantiza hoy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Métodos de string en WASM — ahora sí.&lt;/strong&gt; Un filtro case-insensitive (&lt;code&gt;names.filter(fn(x) =&amp;gt; q == "" or x.lower().contains(q.lower()))&lt;/code&gt;) antes era solo server-rendered; desde Fitz core v0.29.8 los métodos de string (&lt;code&gt;.lower&lt;/code&gt; / &lt;code&gt;.contains&lt;/code&gt; / …) y el &lt;code&gt;and&lt;/code&gt;/&lt;code&gt;or&lt;/code&gt; lógico compilan también en el target client-WASM, así que el filtro corre offline. Lo único que queda es el caret caveat de arriba para &lt;em&gt;la caja del filtro&lt;/em&gt; mientras tipeás.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Qué viene en la serie
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#4+ — Construyendo el flagship.&lt;/strong&gt; Un panel de administración completo en Fitz + Fitz LiveViews: auth por cookie (Argon2id + JWT), DataGrids en vivo consultando Postgres por WebSockets, la librería de UI empaquetada, i18n, y un setup Docker de un comando.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si eventos tipados sin framework de cliente te suena bien, dale una estrella al &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt;. Lo próximo: una app real.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>A native frontend for a compiled language: .fitzv components that compile to WebAssembly</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:47:12 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/a-native-frontend-for-a-compiled-language-fitzv-components-that-compile-to-webassembly-5cfn</link>
      <guid>https://dev.to/martin_palopoli/a-native-frontend-for-a-compiled-language-fitzv-components-that-compile-to-webassembly-5cfn</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 14 of the &lt;strong&gt;Fitz series&lt;/strong&gt;. Part 13 showed the dev loop for &lt;code&gt;.fitzv&lt;/code&gt; → WASM; this part is what a &lt;code&gt;.fitzv&lt;/code&gt; component actually is, and how a language that emits native binaries also emits a frontend — to WebAssembly, without adopting a JavaScript framework.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem: a server language needs a frontend, without importing an ecosystem
&lt;/h2&gt;

&lt;p&gt;Fitz compiles to a native binary and has HTTP/async/Postgres/JWT in the syntax. But a backend eventually needs a UI, and the usual answer is: bolt on the JavaScript ecosystem — a framework (React/Vue/Svelte), a bundler, a &lt;code&gt;node_modules&lt;/code&gt;, a second toolchain, and a second definition of every type that crosses the wire.&lt;/p&gt;

&lt;p&gt;Fitz's answer is a component format the same compiler understands, that compiles &lt;strong&gt;to WebAssembly&lt;/strong&gt; — no JS framework, no npm.&lt;/p&gt;

&lt;h2&gt;
  
  
  A component
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.fitzv&lt;/code&gt; is a single file: state, events, a &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, optional scoped styles — the Vue/Svelte shape, but it's Fitz all the way down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;component Counter {
  state {
    count: Int = 0
  }

  event inc() { count = count + 1 }
  event dec() { count = count - 1 }

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"counter"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"dec"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"n"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{count}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"inc"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;.counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;.n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-variant-numeric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tabular-nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{count}&lt;/code&gt; interpolates state. &lt;code&gt;@click&lt;/code&gt; wires an event. &lt;code&gt;&amp;lt;style scoped&amp;gt;&lt;/code&gt; is scoped to this component (via a hash on the class names). Build it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; web &lt;span class="nt"&gt;--target&lt;/span&gt; wasm-client   &lt;span class="c"&gt;# → target/wasm/web/{web.js, web_bg.wasm}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole toolchain. No &lt;code&gt;package.json&lt;/code&gt;, no bundler config.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the compiler emits
&lt;/h2&gt;

&lt;p&gt;There's no framework runtime in the bundle. The emitter lowers the component directly to Rust over &lt;code&gt;wasm-bindgen&lt;/code&gt; + &lt;code&gt;web-sys&lt;/code&gt;: a struct with the state fields, a &lt;code&gt;mount()&lt;/code&gt; that builds the DOM with &lt;code&gt;create_element&lt;/code&gt;/&lt;code&gt;append_child&lt;/code&gt;, event closures that mutate state, and a &lt;code&gt;render()&lt;/code&gt; that re-paints the component's subtree on a state change. It's a self-contained WASM module — the counter above is &lt;strong&gt;26 KB raw / 11.4 KB gzipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The template is a real DSL: interpolation (&lt;code&gt;{user.name.upper()}&lt;/code&gt; — arbitrary Fitz expressions over state), directives (&lt;code&gt;{#if}&lt;/code&gt; / &lt;code&gt;{#for}&lt;/code&gt;), events (&lt;code&gt;@click&lt;/code&gt; / &lt;code&gt;@input&lt;/code&gt;), and composition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight svelte"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;todo&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;todos&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;TodoRow&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"drop"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;Child prop="v" /&amp;gt;&lt;/code&gt; passes props down; &lt;code&gt;@remove="drop"&lt;/code&gt; bubbles an event up with its payload; &lt;code&gt;&amp;lt;slot&amp;gt;&lt;/code&gt; lets a parent inject content. Composition works across files, so a component library is just imports.&lt;/p&gt;

&lt;h2&gt;
  
  
  The type is the same type
&lt;/h2&gt;

&lt;p&gt;This is the part a bolted-on framework can't give you. The &lt;code&gt;type&lt;/code&gt; your server defines is the exact one your component uses — imported from the same &lt;code&gt;.fitz&lt;/code&gt; module:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// models.fitz
type Todo {
  id: Int
  title: Str
  done: Bool
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fitzv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Todo&lt;/span&gt;
&lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Todo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;todo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;template&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The server compiles &lt;code&gt;Todo&lt;/code&gt; into a native struct; the component compiles it into a WASM struct; there's &lt;strong&gt;one definition&lt;/strong&gt;. No &lt;code&gt;types.ts&lt;/code&gt; that drifts from the backend, no codegen step to keep them in sync — by construction they can't diverge.&lt;/p&gt;

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

&lt;p&gt;Component models aren't new — the point is what compiles them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React / Vue / Svelte&lt;/strong&gt; are excellent, but they're JavaScript: a framework runtime shipped to the browser, a bundler, an &lt;code&gt;npm&lt;/code&gt; tree, and a separate type story for the API boundary. &lt;code&gt;.fitzv&lt;/code&gt; compiles to WASM with none of that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elm&lt;/strong&gt; is a lovely own-language frontend, but it compiles to JavaScript and lives only in the browser — it isn't the same language as your compiled backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust WASM frameworks&lt;/strong&gt; (Dioxus, Leptos) compile to WASM too, but you're adopting a framework, its macro DSL, and its toolchain on top of Rust. In Fitz the component &lt;em&gt;is&lt;/em&gt; the language, and the same &lt;code&gt;fitz&lt;/code&gt; binary builds it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One language, one compiler, one type definition — emitting a native binary for the server and a WASM bundle for the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest edges (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Re-render is &lt;strong&gt;naive&lt;/strong&gt;: a state change re-paints the component's subtree, not a fine-grained signal graph. It's fast enough for the common case; fine-grained reactivity is a future slice.&lt;/li&gt;
&lt;li&gt;The template supports a growing &lt;strong&gt;envelope&lt;/strong&gt; of constructs (interpolation, &lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt;, events, composition, slots, scoped styles); genuinely exotic patterns fall outside it for now and the compiler tells you where.&lt;/li&gt;
&lt;li&gt;The client is &lt;strong&gt;WASM-first&lt;/strong&gt;; there's no JS-vanilla target — a deliberate choice recorded when the counter came in under the 40 KB bundle gate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the frontend as a first-class part of the language: components you write in Fitz, compiled to WebAssembly, sharing types with a backend compiled to native code. Next up: how a function on that backend becomes callable from the component as if it were local — &lt;code&gt;@rpc&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fitz is a compiled language with gradual typing and HTTP/async/DB as first-class citizens, compiling to native binary via Rust. Its frontend is &lt;code&gt;.fitzv&lt;/code&gt; single-file components compiled to WebAssembly — no framework, no npm. Open source.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Un frontend nativo para un lenguaje compilado: componentes .fitzv que compilan a WebAssembly</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:46:26 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/un-frontend-nativo-para-un-lenguaje-compilado-componentes-fitzv-que-compilan-a-webassembly-1e3e</link>
      <guid>https://dev.to/martin_palopoli/un-frontend-nativo-para-un-lenguaje-compilado-componentes-fitzv-que-compilan-a-webassembly-1e3e</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Parte 14 de la &lt;strong&gt;serie Fitz&lt;/strong&gt;. La Parte 13 mostró el dev loop para &lt;code&gt;.fitzv&lt;/code&gt; → WASM; esta parte es qué es realmente un componente &lt;code&gt;.fitzv&lt;/code&gt;, y cómo un lenguaje que emite binarios nativos también emite un frontend — a WebAssembly, sin adoptar un framework de JavaScript.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  El problema: un lenguaje de servidor necesita frontend, sin importar un ecosistema
&lt;/h2&gt;

&lt;p&gt;Fitz compila a binario nativo y tiene HTTP/async/Postgres/JWT en la sintaxis. Pero un backend tarde o temprano necesita una UI, y la respuesta habitual es: pegar el ecosistema JavaScript — un framework (React/Vue/Svelte), un bundler, un &lt;code&gt;node_modules&lt;/code&gt;, un segundo toolchain, y una segunda definición de cada tipo que cruza el cable.&lt;/p&gt;

&lt;p&gt;La respuesta de Fitz es un formato de componentes que el mismo compilador entiende, que compila &lt;strong&gt;a WebAssembly&lt;/strong&gt; — sin framework JS, sin npm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Un componente
&lt;/h2&gt;

&lt;p&gt;Un &lt;code&gt;.fitzv&lt;/code&gt; es un solo archivo: state, events, un &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, estilos scoped opcionales — la forma Vue/Svelte, pero es Fitz de punta a punta:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;component Counter {
  state {
    count: Int = 0
  }

  event inc() { count = count + 1 }
  event dec() { count = count - 1 }

  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"counter"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"dec"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"n"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;{count}&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"inc"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;style &lt;/span&gt;&lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nc"&gt;.counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;.n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;font-variant-numeric&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tabular-nums&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;{count}&lt;/code&gt; interpola el state. &lt;code&gt;@click&lt;/code&gt; cablea un evento. &lt;code&gt;&amp;lt;style scoped&amp;gt;&lt;/code&gt; queda scopeado a este componente (vía un hash en los nombres de clase). Buildealo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; web &lt;span class="nt"&gt;--target&lt;/span&gt; wasm-client   &lt;span class="c"&gt;# → target/wasm/web/{web.js, web_bg.wasm}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese es el toolchain entero. Sin &lt;code&gt;package.json&lt;/code&gt;, sin config de bundler.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué emite el compilador
&lt;/h2&gt;

&lt;p&gt;No hay runtime de framework en el bundle. El emisor baja el componente directo a Rust sobre &lt;code&gt;wasm-bindgen&lt;/code&gt; + &lt;code&gt;web-sys&lt;/code&gt;: un struct con los campos de state, un &lt;code&gt;mount()&lt;/code&gt; que construye el DOM con &lt;code&gt;create_element&lt;/code&gt;/&lt;code&gt;append_child&lt;/code&gt;, closures de evento que mutan el state, y un &lt;code&gt;render()&lt;/code&gt; que re-pinta el subtree del componente ante un cambio de state. Es un módulo WASM autocontenido — el contador de arriba pesa &lt;strong&gt;26 KB raw / 11.4 KB gzipped&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;El template es un DSL de verdad: interpolación (&lt;code&gt;{user.name.upper()}&lt;/code&gt; — expresiones Fitz arbitrarias sobre el state), directivas (&lt;code&gt;{#if}&lt;/code&gt; / &lt;code&gt;{#for}&lt;/code&gt;), eventos (&lt;code&gt;@click&lt;/code&gt; / &lt;code&gt;@input&lt;/code&gt;), y composición:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    {#for todo in todos}
      &lt;span class="nt"&gt;&amp;lt;TodoRow&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"{todo.title}"&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;remove=&lt;/span&gt;&lt;span class="s"&gt;"drop"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    {/for}
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;Child prop="v" /&amp;gt;&lt;/code&gt; pasa props hacia abajo; &lt;code&gt;@remove="drop"&lt;/code&gt; burbujea un evento hacia arriba con su payload; &lt;code&gt;&amp;lt;slot&amp;gt;&lt;/code&gt; deja que un padre inyecte contenido. La composición funciona entre archivos, así que una librería de componentes es solo imports.&lt;/p&gt;

&lt;h2&gt;
  
  
  El tipo es el mismo tipo
&lt;/h2&gt;

&lt;p&gt;Esta es la parte que un framework pegado por encima no te puede dar. El &lt;code&gt;type&lt;/code&gt; que define tu server es exactamente el que usa tu componente — importado del mismo módulo &lt;code&gt;.fitz&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// models.fitz
type Todo {
  id: Int
  title: Str
  done: Bool
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// App.fitzv
from models import Todo
// ... un List&amp;lt;Todo&amp;gt; en el state, {todo.title} en el template
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El server compila &lt;code&gt;Todo&lt;/code&gt; a un struct nativo; el componente lo compila a un struct WASM; hay &lt;strong&gt;una sola definición&lt;/strong&gt;. Sin un &lt;code&gt;types.ts&lt;/code&gt; que se desincroniza del backend, sin un paso de codegen para mantenerlos en sync — por construcción no pueden divergir.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué es distinto
&lt;/h2&gt;

&lt;p&gt;Los modelos de componentes no son nuevos — la cuestión es qué los compila:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React / Vue / Svelte&lt;/strong&gt; son excelentes, pero son JavaScript: un runtime de framework mandado al browser, un bundler, un árbol &lt;code&gt;npm&lt;/code&gt;, y una historia de tipos aparte para el borde de la API. &lt;code&gt;.fitzv&lt;/code&gt; compila a WASM sin nada de eso.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Elm&lt;/strong&gt; es un hermoso frontend de lenguaje propio, pero compila a JavaScript y vive solo en el browser — no es el mismo lenguaje que tu backend compilado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frameworks WASM de Rust&lt;/strong&gt; (Dioxus, Leptos) compilan a WASM también, pero estás adoptando un framework, su DSL de macros, y su toolchain sobre Rust. En Fitz el componente &lt;em&gt;es&lt;/em&gt; el lenguaje, y el mismo binario &lt;code&gt;fitz&lt;/code&gt; lo buildea.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un lenguaje, un compilador, una definición de tipo — emitiendo un binario nativo para el server y un bundle WASM para el browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los bordes honestos (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;El re-render es &lt;strong&gt;naive&lt;/strong&gt;: un cambio de state re-pinta el subtree del componente, no un grafo de señales fine-grained. Alcanza para el caso común; la reactividad fine-grained es un slice futuro.&lt;/li&gt;
&lt;li&gt;El template soporta un &lt;strong&gt;envelope&lt;/strong&gt; creciente de constructos (interpolación, &lt;code&gt;{#if}&lt;/code&gt;/&lt;code&gt;{#for}&lt;/code&gt;, eventos, composición, slots, estilos scoped); los patrones genuinamente exóticos quedan afuera por ahora y el compilador te dice dónde.&lt;/li&gt;
&lt;li&gt;El cliente es &lt;strong&gt;WASM-first&lt;/strong&gt;; no hay target JS-vanilla — una decisión deliberada registrada cuando el contador entró bajo el gate de bundle de 40 KB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese es el frontend como parte de primera clase del lenguaje: componentes que escribís en Fitz, compilados a WebAssembly, compartiendo tipos con un backend compilado a código nativo. Lo que sigue: cómo una función de ese backend se vuelve llamable desde el componente como si fuera local — &lt;code&gt;@rpc&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fitz es un lenguaje compilado con tipado gradual, HTTP/async/DB como ciudadanos de primera clase, que compila a binario nativo vía Rust. Su frontend son componentes single-file &lt;code&gt;.fitzv&lt;/code&gt; compilados a WebAssembly — sin framework, sin npm. Código abierto.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>A Vite-class dev loop for a compiled language: hot reload + state preservation + a live manifest</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:02:16 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/a-vite-class-dev-loop-for-a-compiled-language-hot-reload-state-preservation-a-live-manifest-2ja2</link>
      <guid>https://dev.to/martin_palopoli/a-vite-class-dev-loop-for-a-compiled-language-hot-reload-state-preservation-a-live-manifest-2ja2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Part 13 of the &lt;strong&gt;Fitz series&lt;/strong&gt;. The frontend chapter opens: Fitz compiles &lt;code&gt;.fitzv&lt;/code&gt; components to WebAssembly, and this is the dev loop that makes editing them feel instant — the same "save and see it" experience you get from Vite, on a language that compiles to a native binary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup: a compiled language with a frontend
&lt;/h2&gt;

&lt;p&gt;Fitz is a compiled language — HTTP, async, Postgres, JWT all live in the syntax and it emits a native binary via Rust. The frontend story is a single-file component format, &lt;code&gt;.fitzv&lt;/code&gt; (state + events + &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, à la Vue/Svelte), that compiles &lt;strong&gt;to WebAssembly&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; web &lt;span class="nt"&gt;--target&lt;/span&gt; wasm-client   &lt;span class="c"&gt;# → target/wasm/web/{web.js, web_bg.wasm}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;npm install&lt;/code&gt;, no bundler config, no external framework — the component becomes a self-contained WASM bundle (the counter demo is 11.4 KB gzipped).&lt;/p&gt;

&lt;p&gt;Here's the reflex objection: &lt;em&gt;compiled means slow feedback&lt;/em&gt;. Edit, wait for a full compile, refresh the browser by hand. That's the opposite of what a frontend loop should feel like. So Fitz has &lt;code&gt;fitz dev&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop
&lt;/h2&gt;

&lt;p&gt;Point &lt;code&gt;fitz dev&lt;/code&gt; at a &lt;code&gt;wasm-client&lt;/code&gt; bin and it stops being a compiler and starts being a dev server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz dev              &lt;span class="c"&gt;# serves on http://127.0.0.1:1234/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Incremental rebuild&lt;/strong&gt; with &lt;code&gt;wasm-pack --dev&lt;/code&gt; (no &lt;code&gt;wasm-opt&lt;/code&gt;), reusing a stable crate so cargo's cache stays warm — the first build compiles the deps, every save after that is &lt;strong&gt;~1-2 seconds&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A dev server&lt;/strong&gt; that serves your project root like &lt;code&gt;python -m http.server&lt;/code&gt;: your &lt;code&gt;index.html&lt;/code&gt;, your CSS, the bundle in &lt;code&gt;target/wasm/&amp;lt;bin&amp;gt;/&lt;/code&gt;. No &lt;code&gt;index.html&lt;/code&gt;? It generates a minimal one at the mount point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser auto-refresh over a WebSocket&lt;/strong&gt;: save a &lt;code&gt;.fitzv&lt;/code&gt;/&lt;code&gt;.fitz&lt;/code&gt;/&lt;code&gt;fitz.toml&lt;/code&gt; and the page reloads itself. No manual F5.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save, and ~2 seconds later the browser shows the change. On a compiled language.&lt;/p&gt;

&lt;h2&gt;
  
  
  The detail that matters: state survives the reload
&lt;/h2&gt;

&lt;p&gt;Most hot-reload stories lose your state on a full reload — you were three clicks into a counter, you edit the template, and you're back at zero. &lt;code&gt;fitz dev&lt;/code&gt; snapshots the component's live state before the reload and restores it after:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bump &lt;code&gt;count&lt;/code&gt; to 7, type two items into the list, then edit the &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; above them. The reload lands and &lt;code&gt;count&lt;/code&gt; is still 7, the list still has its two items. Primitives, lists, maps, nullables, custom nominal types — all survive (serialized to JSON into &lt;code&gt;sessionStorage&lt;/code&gt;, restored on boot). You keep editing against the exact state you were testing.&lt;/p&gt;

&lt;p&gt;(Honest caveat: a preserved state field wins over a changed &lt;em&gt;default&lt;/em&gt; in the template — the state is what's preserved. Change a default and reset to see it.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The manifest re-resolves live too
&lt;/h2&gt;

&lt;p&gt;This is the piece that just shipped. Editing a &lt;code&gt;.fitzv&lt;/code&gt; was always picked up (the entry is re-read every build) — but the &lt;em&gt;manifest&lt;/em&gt; wasn't. Now it is: save &lt;code&gt;fitz.toml&lt;/code&gt; and &lt;code&gt;fitz dev&lt;/code&gt; re-resolves it without restarting.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repoint &lt;code&gt;[bin].main&lt;/code&gt;&lt;/strong&gt; to a different component → next rebuild uses the new entry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add a &lt;code&gt;[dependencies]&lt;/code&gt;&lt;/strong&gt; — pull in a companion-UI component (&lt;code&gt;from fitz_liveviews.ui.Badge import Badge&lt;/code&gt;) — → resolved and available on the next build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edit &lt;code&gt;[flags]&lt;/code&gt;&lt;/strong&gt; → picked up live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a &lt;code&gt;fitz.toml&lt;/code&gt; you broke mid-edit doesn't kill the loop: it prints the parse error and &lt;strong&gt;keeps serving the previous bundle&lt;/strong&gt;, recovering on your next valid save.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;↻ change in fitz.toml — rebuilding ...
↻ fitz.toml re-resolved
✓ reloaded browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fullstack: two bins, two terminals
&lt;/h2&gt;

&lt;p&gt;A real app is a backend plus a frontend. In Fitz that's two bins in one manifest — a native &lt;code&gt;server&lt;/code&gt; and a &lt;code&gt;wasm-client&lt;/code&gt; &lt;code&gt;web&lt;/code&gt; — wired with &lt;code&gt;@rpc&lt;/code&gt; (server functions callable from the component as if they were local). You dev each half in its own terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1 — the backend&lt;/span&gt;
fitz run &lt;span class="nt"&gt;--bin&lt;/span&gt; server

&lt;span class="c"&gt;# Terminal 2 — the frontend, hot reload&lt;/span&gt;
fitz dev &lt;span class="nt"&gt;--bin&lt;/span&gt; web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--bin &amp;lt;name&amp;gt;&lt;/code&gt; selects the bin in a multi-bin project; it lands on &lt;code&gt;fitz run&lt;/code&gt; and &lt;code&gt;fitz build&lt;/code&gt; too.&lt;/p&gt;

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

&lt;p&gt;Live-reload loops aren't new — the point is &lt;em&gt;what it takes to get one&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vite / webpack HMR&lt;/strong&gt; give a great loop, but they're the JavaScript ecosystem: a bundler, a &lt;code&gt;node_modules&lt;/code&gt;, a framework runtime. Fitz's loop is one CLI over a language that compiles to WASM — no npm at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cargo watch&lt;/code&gt;&lt;/strong&gt; rebuilds and restarts, but there's no browser reload and no state preservation — you refresh by hand and start over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust WASM frameworks&lt;/strong&gt; (Dioxus, Leptos) have hot-reload stories, but you're adopting a framework + its macro DSL + its toolchain. Here the component &lt;em&gt;is&lt;/em&gt; the language, and &lt;code&gt;fitz dev&lt;/code&gt; ships with the compiler.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The combination — a compiled-to-WASM language, incremental rebuilds, browser auto-refresh, state that survives the reload, and a manifest that re-resolves live — is the part no single tool in the neighborhood gives you out of one binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest edges (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It &lt;strong&gt;recompiles the crate&lt;/strong&gt; (incrementally — "Approach C"). It is &lt;em&gt;not&lt;/em&gt; yet a data-driven template runtime that diffs without recompiling; sub-second-without-a-rebuild is a bigger future north. For the 90% visual loop, ~2s feels the same.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;classic native/SSR mode&lt;/strong&gt; of &lt;code&gt;fitz dev&lt;/code&gt; is kill+respawn without browser reload (there's no page to refresh) — the wasm-client mode above is where the browser loop lives.&lt;/li&gt;
&lt;li&gt;Running &lt;strong&gt;both bins in one &lt;code&gt;fitz dev&lt;/code&gt;&lt;/strong&gt; (dual-process orchestration) is a follow-up; today it's two terminals.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's the frontend dev loop closed: a compiled language whose components you edit and see in the browser in a couple of seconds, without losing your state, without an npm ecosystem underneath. Next up: how the same &lt;code&gt;.fitzv&lt;/code&gt; renders on the server for first paint, and the WASM runtime then adopts that DOM instead of re-creating it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fitz is a compiled language with gradual typing and HTTP/async/DB as first-class citizens, compiling to native binary via Rust. Its frontend is &lt;code&gt;.fitzv&lt;/code&gt; single-file components compiled to WebAssembly. Open source.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Un dev loop tipo Vite para un lenguaje compilado: hot reload + preservación de state + manifest en vivo</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:00:50 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/un-dev-loop-tipo-vite-para-un-lenguaje-compilado-hot-reload-preservacion-de-state-manifest-en-1lj8</link>
      <guid>https://dev.to/martin_palopoli/un-dev-loop-tipo-vite-para-un-lenguaje-compilado-hot-reload-preservacion-de-state-manifest-en-1lj8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Parte 13 de la &lt;strong&gt;serie Fitz&lt;/strong&gt;. Se abre el capítulo del frontend: Fitz compila componentes &lt;code&gt;.fitzv&lt;/code&gt; a WebAssembly, y este es el dev loop que hace que editarlos se sienta instantáneo — la misma experiencia "guardar y verlo" que te da Vite, sobre un lenguaje que compila a binario nativo.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  El setup: un lenguaje compilado con frontend
&lt;/h2&gt;

&lt;p&gt;Fitz es un lenguaje compilado — HTTP, async, Postgres, JWT viven en la sintaxis y emite un binario nativo vía Rust. La historia del frontend es un formato de componentes single-file, &lt;code&gt;.fitzv&lt;/code&gt; (state + events + &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, al estilo Vue/Svelte), que compila &lt;strong&gt;a WebAssembly&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; web &lt;span class="nt"&gt;--target&lt;/span&gt; wasm-client   &lt;span class="c"&gt;# → target/wasm/web/{web.js, web_bg.wasm}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sin &lt;code&gt;npm install&lt;/code&gt;, sin config de bundler, sin framework externo — el componente se vuelve un bundle WASM autocontenido (el demo del contador pesa 11.4 KB gzipped).&lt;/p&gt;

&lt;p&gt;Acá viene la objeción refleja: &lt;em&gt;compilado = feedback lento&lt;/em&gt;. Editás, esperás una compilación entera, refrescás el browser a mano. Es lo opuesto a lo que un loop de frontend debería sentirse. Por eso Fitz tiene &lt;code&gt;fitz dev&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  El loop
&lt;/h2&gt;

&lt;p&gt;Apuntá &lt;code&gt;fitz dev&lt;/code&gt; a un bin &lt;code&gt;wasm-client&lt;/code&gt; y deja de ser un compilador para ser un dev server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz dev              &lt;span class="c"&gt;# sirve en http://127.0.0.1:1234/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Qué hace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rebuild incremental&lt;/strong&gt; con &lt;code&gt;wasm-pack --dev&lt;/code&gt; (sin &lt;code&gt;wasm-opt&lt;/code&gt;), reusando un crate estable así la cache de cargo queda caliente — el primer build compila las deps, cada save siguiente es de &lt;strong&gt;~1-2 segundos&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un dev server&lt;/strong&gt; que sirve el root de tu proyecto como &lt;code&gt;python -m http.server&lt;/code&gt;: tu &lt;code&gt;index.html&lt;/code&gt;, tu CSS, el bundle en &lt;code&gt;target/wasm/&amp;lt;bin&amp;gt;/&lt;/code&gt;. ¿Sin &lt;code&gt;index.html&lt;/code&gt;? Genera uno mínimo en el punto de &lt;code&gt;mount&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-refresh del browser por WebSocket&lt;/strong&gt;: guardás un &lt;code&gt;.fitzv&lt;/code&gt;/&lt;code&gt;.fitz&lt;/code&gt;/&lt;code&gt;fitz.toml&lt;/code&gt; y la página se recarga sola. Sin F5 a mano.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Guardás, y ~2 segundos después el browser muestra el cambio. En un lenguaje compilado.&lt;/p&gt;

&lt;h2&gt;
  
  
  El detalle que importa: el state sobrevive el reload
&lt;/h2&gt;

&lt;p&gt;La mayoría de los hot-reload pierden tu estado en un reload completo — ibas tres clicks adentro de un contador, editás el template, y volvés a cero. &lt;code&gt;fitz dev&lt;/code&gt; snapshotea el estado vivo del componente antes del reload y lo restaura después:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Str&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subí &lt;code&gt;count&lt;/code&gt; a 7, escribí dos items en la lista, después editá el &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; de arriba. El reload aterriza y &lt;code&gt;count&lt;/code&gt; sigue en 7, la lista sigue con sus dos items. Primitivos, listas, mapas, nullables, tipos nominales custom — todos sobreviven (serializados a JSON en &lt;code&gt;sessionStorage&lt;/code&gt;, restaurados al boot). Seguís editando contra el estado exacto con el que estabas probando.&lt;/p&gt;

&lt;p&gt;(Caveat honesto: un campo de state preservado gana ante un &lt;em&gt;default&lt;/em&gt; cambiado en el template — lo que se preserva es el estado. Cambiá un default y reseteá para verlo.)&lt;/p&gt;

&lt;h2&gt;
  
  
  El manifest también se re-resuelve en vivo
&lt;/h2&gt;

&lt;p&gt;Esta es la pieza que acaba de salir. Editar un &lt;code&gt;.fitzv&lt;/code&gt; siempre se tomaba (el entry se re-lee en cada build) — pero el &lt;em&gt;manifest&lt;/em&gt; no. Ahora sí: guardás &lt;code&gt;fitz.toml&lt;/code&gt; y &lt;code&gt;fitz dev&lt;/code&gt; lo re-resuelve sin reiniciar.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repuntá &lt;code&gt;[bin].main&lt;/code&gt;&lt;/strong&gt; a otro componente → el próximo rebuild usa el nuevo entry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agregá una &lt;code&gt;[dependencies]&lt;/code&gt;&lt;/strong&gt; — traé un componente de la companion UI (&lt;code&gt;from fitz_liveviews.ui.Badge import Badge&lt;/code&gt;) — → resuelto y disponible en el próximo build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editá &lt;code&gt;[flags]&lt;/code&gt;&lt;/strong&gt; → se toma en vivo.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y un &lt;code&gt;fitz.toml&lt;/code&gt; que rompiste a mitad de edición no mata el loop: imprime el error de parse y &lt;strong&gt;sigue sirviendo el bundle anterior&lt;/strong&gt;, recuperándose en tu próximo save válido.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;↻ change in fitz.toml — rebuilding ...
↻ fitz.toml re-resolved
✓ reloaded browser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Fullstack: dos bins, dos terminales
&lt;/h2&gt;

&lt;p&gt;Una app real es un backend más un frontend. En Fitz eso son dos bins en un manifest — un &lt;code&gt;server&lt;/code&gt; nativo y un &lt;code&gt;web&lt;/code&gt; &lt;code&gt;wasm-client&lt;/code&gt; — cableados con &lt;code&gt;@rpc&lt;/code&gt; (funciones de servidor llamables desde el componente como si fueran locales). Dev-eás cada mitad en su propia terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal 1 — el backend&lt;/span&gt;
fitz run &lt;span class="nt"&gt;--bin&lt;/span&gt; server

&lt;span class="c"&gt;# Terminal 2 — el frontend, hot reload&lt;/span&gt;
fitz dev &lt;span class="nt"&gt;--bin&lt;/span&gt; web
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--bin &amp;lt;nombre&amp;gt;&lt;/code&gt; elige el bin en un proyecto multi-bin; existe igual en &lt;code&gt;fitz run&lt;/code&gt; y &lt;code&gt;fitz build&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué es distinto
&lt;/h2&gt;

&lt;p&gt;Los loops de live-reload no son nuevos — la cuestión es &lt;em&gt;qué hace falta para tener uno&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Vite / webpack HMR&lt;/strong&gt; dan un loop excelente, pero son el ecosistema JavaScript: un bundler, un &lt;code&gt;node_modules&lt;/code&gt;, un runtime de framework. El loop de Fitz es un CLI sobre un lenguaje que compila a WASM — sin npm en absoluto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cargo watch&lt;/code&gt;&lt;/strong&gt; rebuildea y reinicia, pero no hay reload del browser ni preservación de state — refrescás a mano y arrancás de nuevo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frameworks WASM de Rust&lt;/strong&gt; (Dioxus, Leptos) tienen hot-reload, pero estás adoptando un framework + su DSL de macros + su toolchain. Acá el componente &lt;em&gt;es&lt;/em&gt; el lenguaje, y &lt;code&gt;fitz dev&lt;/code&gt; viene con el compilador.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La combinación — un lenguaje compilado a WASM, rebuilds incrementales, auto-refresh del browser, state que sobrevive el reload, y un manifest que se re-resuelve en vivo — es la parte que ninguna herramienta sola del vecindario te da desde un solo binario.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los bordes honestos (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Recompila el crate&lt;/strong&gt; (incremental — "Approach C"). &lt;em&gt;No&lt;/em&gt; es todavía un runtime de template data-driven que aplique diffs sin recompilar; sub-segundo-sin-rebuild es un norte futuro más grande. Para el 90% del loop visual, ~2s se siente igual.&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;modo clásico native/SSR&lt;/strong&gt; de &lt;code&gt;fitz dev&lt;/code&gt; es kill+respawn sin reload del browser (no hay página que refrescar) — el modo wasm-client de arriba es donde vive el loop del browser.&lt;/li&gt;
&lt;li&gt;Correr &lt;strong&gt;ambos bins en UN &lt;code&gt;fitz dev&lt;/code&gt;&lt;/strong&gt; (orquestación dual-process) es un follow-up; hoy son dos terminales.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ese es el dev loop del frontend cerrado: un lenguaje compilado cuyos componentes editás y ves en el browser en un par de segundos, sin perder tu estado, sin un ecosistema npm abajo. Lo que sigue: cómo el mismo &lt;code&gt;.fitzv&lt;/code&gt; se renderiza en el server para el first paint, y el runtime WASM después adopta ese DOM en vez de recrearlo.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fitz es un lenguaje compilado con tipado gradual, HTTP/async/DB como ciudadanos de primera clase, que compila a binario nativo vía Rust. Su frontend son componentes single-file &lt;code&gt;.fitzv&lt;/code&gt; compilados a WebAssembly. Código abierto.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Your first Fitz LiveViews component, twice: SSR and WASM from one source</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:59:23 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/your-first-fitz-liveviews-component-twice-ssr-and-wasm-from-one-source-1hlo</link>
      <guid>https://dev.to/martin_palopoli/your-first-fitz-liveviews-component-twice-ssr-and-wasm-from-one-source-1hlo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — A &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;Fitz LiveViews&lt;/a&gt; component is a single &lt;code&gt;.fitzv&lt;/code&gt; file. The interesting part: &lt;strong&gt;the same file compiles to two different targets&lt;/strong&gt; with no rewrite. &lt;strong&gt;Server-rendered&lt;/strong&gt; (SSR) — the server holds the state, renders HTML, and patches the browser over a WebSocket; best for shared, DB-driven, multi-user state. &lt;strong&gt;Client-WASM&lt;/strong&gt; — the same component compiles to WebAssembly and runs entirely in the browser; best for offline, zero-round-trip widgets. This post builds a counter and ships it both ways. &lt;em&gt;(Part 2 of the FitzLiveViews series — &lt;a href="https://dev.to/"&gt;start here&lt;/a&gt; if you missed part 1.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In &lt;a href="https://dev.to/"&gt;part 1&lt;/a&gt; I made the pitch: real-time UI in one language, no JavaScript build. Now let's build something and ship it two ways from the same source.&lt;/p&gt;

&lt;h2&gt;
  
  
  The component
&lt;/h2&gt;

&lt;p&gt;Here's a counter as a single-file component (&lt;code&gt;.fitzv&lt;/code&gt;) — state, events, template, style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;component Counter {
  state { count: Int = 0 }

  event increment() { count = count + 1 }
  event decrement() { count = count - 1 }
  event reset() { count = 0 }

  &lt;span class="nt"&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"counter-app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Count: {count}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"increment"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+1&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"decrement"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-1&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"reset"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reset&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;style &lt;/span&gt;&lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;#counter-app&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;state&lt;/code&gt; is the reactive data. Each &lt;code&gt;event&lt;/code&gt; handler mutates it directly — no &lt;code&gt;setState&lt;/code&gt;, no reducers. &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; is real markup; &lt;code&gt;{count}&lt;/code&gt; interpolates and auto-escapes. &lt;code&gt;@click="increment"&lt;/code&gt; binds a DOM event to a handler. &lt;code&gt;&amp;lt;style scoped&amp;gt;&lt;/code&gt; is CSS namespaced to this component. If you've written Vue or Svelte, this is familiar — the difference is what happens next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Target 1 — server-rendered (over a WebSocket)
&lt;/h2&gt;

&lt;p&gt;The SSR target is the default. The component runs on the server; a tiny &lt;code&gt;main.fitz&lt;/code&gt; wires it into an HTTP route (first paint) and a WebSocket route (the live layer):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatch_component_events&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Counter_increment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter_decrement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter_reset&lt;/span&gt;

&lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;counter-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WsConn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
  &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;the&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What happens on a click: the browser sends the event over the WebSocket → &lt;code&gt;dispatch_component_events&lt;/code&gt; routes it to the handler → the handler mutates state → the server recomputes the HTML, &lt;strong&gt;diffs it against the previous render, and sends only the patches&lt;/strong&gt;. The browser applies them. You never wrote client JavaScript, never defined an API, never serialized a payload. The state lives on the server, so it survives reloads and can be shared or DB-backed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fitz run&lt;/code&gt;, open &lt;code&gt;http://localhost:3000&lt;/code&gt;, click. That's the whole app — three files (&lt;code&gt;Counter.fitzv&lt;/code&gt;, &lt;code&gt;main.fitz&lt;/code&gt;, &lt;code&gt;fitz.toml&lt;/code&gt;), and it's exactly what you see above. The complete, ready-to-run version is in the repo at &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/counter" rel="noopener noreferrer"&gt;&lt;code&gt;examples/counter/&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;git clone&lt;/code&gt;, &lt;code&gt;cd examples/counter&lt;/code&gt;, &lt;code&gt;fitz run&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Target 2 — client-WASM (offline, no server)
&lt;/h2&gt;

&lt;p&gt;Now the &lt;em&gt;same &lt;code&gt;Counter.fitzv&lt;/code&gt;&lt;/em&gt; — no changes — compiled to WebAssembly. Declare it as a wasm bin in &lt;code&gt;fitz.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[bin]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"counter"&lt;/span&gt;
&lt;span class="py"&gt;main&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Counter.fitzv"&lt;/span&gt;
&lt;span class="py"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"wasm-client"&lt;/span&gt;
&lt;span class="py"&gt;mount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#app"&lt;/span&gt;       &lt;span class="c"&gt;# required: where the component mounts in the page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; counter &lt;span class="nt"&gt;--target&lt;/span&gt; wasm-client
&lt;span class="c"&gt;# → emits a wasm-bindgen + web-sys crate, runs wasm-pack,&lt;/span&gt;
&lt;span class="c"&gt;#   produces counter.js + counter_bg.wasm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop the two files into a page with &lt;code&gt;&amp;lt;div id="app"&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt; and a &lt;code&gt;&amp;lt;script type="module"&amp;gt;import init from './counter.js'; init()&amp;lt;/script&amp;gt;&lt;/code&gt;, and the counter runs &lt;strong&gt;entirely in the browser&lt;/strong&gt; — no server, no WebSocket. The state lives in &lt;code&gt;Rc&amp;lt;RefCell&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt; cells; &lt;code&gt;@click&lt;/code&gt; handlers mutate the DOM directly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's running right now&lt;/strong&gt; in the &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/" rel="noopener noreferrer"&gt;live component gallery&lt;/a&gt; — that counter is this exact file compiled to WASM. The bundle is &lt;strong&gt;11.4 KB gzipped&lt;/strong&gt; for the whole thing (no separate framework runtime to load first).&lt;/p&gt;

&lt;h2&gt;
  
  
  Same source — so which target?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Server-rendered&lt;/th&gt;
&lt;th&gt;Client-WASM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;State lives&lt;/td&gt;
&lt;td&gt;on the server&lt;/td&gt;
&lt;td&gt;in the browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-user / shared / DB-backed&lt;/td&gt;
&lt;td&gt;✅ natural&lt;/td&gt;
&lt;td&gt;❌ (per-tab)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works offline&lt;/td&gt;
&lt;td&gt;❌ (needs the socket)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round-trip per interaction&lt;/td&gt;
&lt;td&gt;one WS message&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;dashboards, CRUD, chat, anything shared&lt;/td&gt;
&lt;td&gt;widgets, calculators, offline tools&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point isn't "pick one forever." It's that a presentational component — a badge, a card, a stepper — can serve both, and you decide per component. Shared state? SSR. Local, offline, zero-latency? WASM. Same &lt;code&gt;.fitzv&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next in this series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#3 — Forms, payloads, and live inputs.&lt;/strong&gt; How events carry data: click payloads, form submits, and the &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt; value binding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;#4+ — Building the flagship.&lt;/strong&gt; A real admin panel: cookie auth, live DataGrids over Postgres, i18n, and Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Star the &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt; if the one-source-two-targets idea clicks. Next: forms and payloads.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Tu primer componente de Fitz LiveViews, dos veces: SSR y WASM del mismo source</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Thu, 06 Aug 2026 09:58:16 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/tu-primer-componente-de-fitz-liveviews-dos-veces-ssr-y-wasm-del-mismo-source-295a</link>
      <guid>https://dev.to/martin_palopoli/tu-primer-componente-de-fitz-liveviews-dos-veces-ssr-y-wasm-del-mismo-source-295a</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — Un componente de &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;Fitz LiveViews&lt;/a&gt; es un solo archivo &lt;code&gt;.fitzv&lt;/code&gt;. Lo interesante: &lt;strong&gt;el mismo archivo compila a dos targets distintos&lt;/strong&gt; sin reescribir nada. &lt;strong&gt;Server-rendered&lt;/strong&gt; (SSR) — el servidor guarda el estado, renderiza HTML, y parchea el browser por WebSocket; ideal para estado compartido, DB-driven, multi-usuario. &lt;strong&gt;Client-WASM&lt;/strong&gt; — el mismo componente compila a WebAssembly y corre entero en el browser; ideal para widgets offline sin round-trip. Este post construye un counter y lo shipea de las dos formas. &lt;em&gt;(Parte 2 de la serie FitzLiveViews — &lt;a href="https://dev.to/"&gt;empezá acá&lt;/a&gt; si te perdiste la parte 1.)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;En la &lt;a href="https://dev.to/"&gt;parte 1&lt;/a&gt; hice el pitch: UI en tiempo real en un solo lenguaje, sin build de JavaScript. Ahora construyamos algo y shipeémoslo de dos formas desde el mismo source.&lt;/p&gt;

&lt;h2&gt;
  
  
  El componente
&lt;/h2&gt;

&lt;p&gt;Un counter como componente de un solo archivo (&lt;code&gt;.fitzv&lt;/code&gt;) — state, events, template, style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;component Counter {
  state { count: Int = 0 }

  event increment() { count = count + 1 }
  event decrement() { count = count - 1 }
  event reset() { count = 0 }

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"counter-app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Count: {count}&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"increment"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;+1&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"decrement"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;-1&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;click=&lt;/span&gt;&lt;span class="s"&gt;"reset"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reset&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt; &lt;span class="na"&gt;scoped&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nf"&gt;#counter-app&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;system-ui&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;style&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;state&lt;/code&gt; es la data reactiva. Cada handler de &lt;code&gt;event&lt;/code&gt; la muta directo — sin &lt;code&gt;setState&lt;/code&gt;, sin reducers. &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; es markup real; &lt;code&gt;{count}&lt;/code&gt; interpola y auto-escapa. &lt;code&gt;@click="increment"&lt;/code&gt; bindea un evento del DOM a un handler. &lt;code&gt;&amp;lt;style scoped&amp;gt;&lt;/code&gt; es CSS namespaceado a este componente. Si escribiste Vue o Svelte, esto te suena — la diferencia es lo que pasa después.&lt;/p&gt;

&lt;h2&gt;
  
  
  Target 1 — server-rendered (por WebSocket)
&lt;/h2&gt;

&lt;p&gt;El target SSR es el default. El componente corre en el servidor; un &lt;code&gt;main.fitz&lt;/code&gt; chiquito lo cablea a una ruta HTTP (primer paint) y una ruta WebSocket (la capa en vivo):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fitz_liveviews&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dispatch_component_events&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter_render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;Counter_increment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter_decrement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Counter_reset&lt;/span&gt;

&lt;span class="nd"&gt;@get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;html_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;live_layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;counter-app&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@ws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/live/counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WsConn&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
  &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dispatch_component_events&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;rutea&lt;/span&gt; &lt;span class="n"&gt;el&lt;/span&gt; &lt;span class="n"&gt;evento&lt;/span&gt; &lt;span class="n"&gt;al&lt;/span&gt; &lt;span class="n"&gt;componente&lt;/span&gt;
    &lt;span class="n"&gt;let&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Counter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;
    &lt;span class="n"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LiveFrame&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;diff_html&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;
    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_html&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Qué pasa en un click: el browser manda el evento por el WebSocket → &lt;code&gt;dispatch_component_events&lt;/code&gt; lo rutea al handler → el handler muta el estado → el servidor recomputa el HTML, &lt;strong&gt;lo diffea contra el render anterior, y manda solo los parches&lt;/strong&gt;. El browser los aplica. Nunca escribiste JavaScript de cliente, nunca definiste una API, nunca serializaste un payload. El estado vive en el servidor, así que sobrevive reloads y puede ser compartido o respaldado por DB.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;fitz run&lt;/code&gt;, abrís &lt;code&gt;http://localhost:3000&lt;/code&gt;, clickeás. Esa es toda la app — tres archivos (&lt;code&gt;Counter.fitzv&lt;/code&gt;, &lt;code&gt;main.fitz&lt;/code&gt;, &lt;code&gt;fitz.toml&lt;/code&gt;), y es exactamente lo que ves arriba. La versión completa y lista para correr está en el repo en &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/tree/main/examples/counter" rel="noopener noreferrer"&gt;&lt;code&gt;examples/counter/&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;git clone&lt;/code&gt;, &lt;code&gt;cd examples/counter&lt;/code&gt;, &lt;code&gt;fitz run&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Target 2 — client-WASM (offline, sin servidor)
&lt;/h2&gt;

&lt;p&gt;Ahora el &lt;em&gt;mismo &lt;code&gt;Counter.fitzv&lt;/code&gt;&lt;/em&gt; — sin cambios — compilado a WebAssembly. Declaralo como un bin wasm en &lt;code&gt;fitz.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[[bin]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"counter"&lt;/span&gt;
&lt;span class="py"&gt;main&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Counter.fitzv"&lt;/span&gt;
&lt;span class="py"&gt;target&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"wasm-client"&lt;/span&gt;
&lt;span class="py"&gt;mount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#app"&lt;/span&gt;       &lt;span class="c"&gt;# requerido: dónde monta el componente en la página&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;fitz build &lt;span class="nt"&gt;--bin&lt;/span&gt; counter &lt;span class="nt"&gt;--target&lt;/span&gt; wasm-client
&lt;span class="c"&gt;# → emite un crate wasm-bindgen + web-sys, corre wasm-pack,&lt;/span&gt;
&lt;span class="c"&gt;#   produce counter.js + counter_bg.wasm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Metés los dos archivos en una página con &lt;code&gt;&amp;lt;div id="app"&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt; y un &lt;code&gt;&amp;lt;script type="module"&amp;gt;import init from './counter.js'; init()&amp;lt;/script&amp;gt;&lt;/code&gt;, y el counter corre &lt;strong&gt;entero en el browser&lt;/strong&gt; — sin servidor, sin WebSocket. El estado vive en celdas &lt;code&gt;Rc&amp;lt;RefCell&amp;lt;T&amp;gt;&amp;gt;&lt;/code&gt;; los handlers de &lt;code&gt;@click&lt;/code&gt; mutan el DOM directo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Está corriendo ahora mismo&lt;/strong&gt; en la &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/" rel="noopener noreferrer"&gt;galería de componentes en vivo&lt;/a&gt; — ese counter es este archivo exacto compilado a WASM. El bundle son &lt;strong&gt;11.4 KB gzipped&lt;/strong&gt; por todo (sin runtime de framework aparte que bajar primero).&lt;/p&gt;

&lt;h2&gt;
  
  
  Mismo source — ¿entonces qué target?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Server-rendered&lt;/th&gt;
&lt;th&gt;Client-WASM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dónde vive el estado&lt;/td&gt;
&lt;td&gt;en el servidor&lt;/td&gt;
&lt;td&gt;en el browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-usuario / compartido / DB&lt;/td&gt;
&lt;td&gt;✅ natural&lt;/td&gt;
&lt;td&gt;❌ (por pestaña)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Funciona offline&lt;/td&gt;
&lt;td&gt;❌ (necesita el socket)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round-trip por interacción&lt;/td&gt;
&lt;td&gt;un mensaje WS&lt;/td&gt;
&lt;td&gt;ninguno&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ideal para&lt;/td&gt;
&lt;td&gt;dashboards, CRUD, chat, cualquier cosa compartida&lt;/td&gt;
&lt;td&gt;widgets, calculadoras, herramientas offline&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El punto no es "elegí uno para siempre". Es que un componente presentacional — un badge, una card, un stepper — puede servir a los dos, y decidís por componente. ¿Estado compartido? SSR. ¿Local, offline, cero latencia? WASM. El mismo &lt;code&gt;.fitzv&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué viene en la serie
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#3 — Forms, payloads e inputs en vivo.&lt;/strong&gt; Cómo los eventos llevan data: click payloads, form submits, y el binding de valores con &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;#4+ — Construyendo el flagship.&lt;/strong&gt; Un panel de administración real: auth por cookie, DataGrids en vivo sobre Postgres, i18n, y Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dale una estrella al &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt; si te cierra la idea de un-source-dos-targets. Lo próximo: forms y payloads.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Introducing Fitz LiveViews: real-time UI in one language, zero JS build</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Sat, 01 Aug 2026 12:09:55 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/introducing-fitz-liveviews-real-time-ui-in-one-language-zero-js-build-14dk</link>
      <guid>https://dev.to/martin_palopoli/introducing-fitz-liveviews-real-time-ui-in-one-language-zero-js-build-14dk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — &lt;strong&gt;Fitz LiveViews&lt;/strong&gt; is a real-time UI framework for &lt;a href="https://github.com/Thegreekman76/fitz" rel="noopener noreferrer"&gt;Fitz&lt;/a&gt;, a compiled, gradually-typed language where HTTP, WebSockets, auth, and an ORM are part of the syntax. You write single-file components (&lt;code&gt;.fitzv&lt;/code&gt;) with &lt;code&gt;state&lt;/code&gt; / &lt;code&gt;event&lt;/code&gt; / &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, and the server renders HTML, diffs it, and patches the browser over a WebSocket — &lt;strong&gt;no JavaScript build step, no client framework&lt;/strong&gt;. The same &lt;code&gt;.fitzv&lt;/code&gt; can &lt;em&gt;also&lt;/em&gt; compile to WebAssembly for offline, zero-round-trip widgets. There's a live component gallery, a course, and a full flagship app (an admin panel with auth + Postgres + Docker) already built with it. &lt;strong&gt;Repo&lt;/strong&gt;: &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;github.com/Thegreekman76/fitz-liveviews&lt;/a&gt; · &lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://thegreekman76.github.io/fitz-liveviews/" rel="noopener noreferrer"&gt;thegreekman76.github.io/fitz-liveviews&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the first post in the &lt;strong&gt;FitzLiveViews&lt;/strong&gt; series. I'll start with the pitch and the setup; the following posts build things.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Building a modern web UI usually means two languages, two type systems, and a build pipeline: a backend (Python / Node / Go) plus a frontend framework (React / Vue / Svelte) plus its toolchain (Vite / Webpack / Babel). You duplicate your types across the wire, you keep two mental models in sync, and &lt;code&gt;node_modules&lt;/code&gt; grows a personality of its own.&lt;/p&gt;

&lt;p&gt;Phoenix LiveView (Elixir) showed there's another way: render on the server, push diffs over a WebSocket, and let the browser stay dumb. No client framework, no API to hand-write, no JSON serialization dance. Fitz LiveViews brings that model to Fitz — and adds a twist: the &lt;em&gt;same component&lt;/em&gt; can also compile to WebAssembly when you want purely client-side, offline interactivity.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Fitz LiveViews looks like
&lt;/h2&gt;

&lt;p&gt;A component is a single &lt;code&gt;.fitzv&lt;/code&gt; file — state, event handlers, and a template, like Vue or Svelte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;counter-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;increment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;decrement&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reset&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Reset&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/template&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the &lt;strong&gt;server-rendered&lt;/strong&gt; target, the framework registers this component, sends the initial HTML, and — on every event — recomputes the HTML, diffs it, and sends only the patches over a WebSocket. The browser applies them. You never write client JavaScript, never define an API, never serialize a payload by hand.&lt;/p&gt;

&lt;p&gt;On the &lt;strong&gt;client-WASM&lt;/strong&gt; target (&lt;code&gt;fitz build --target wasm-client&lt;/code&gt;), the &lt;em&gt;same file&lt;/em&gt; compiles to a &lt;code&gt;wasm-bindgen&lt;/code&gt; + &lt;code&gt;web-sys&lt;/code&gt; bundle that mounts and mutates the DOM directly — offline, no server. A single component is ~12 KB gzipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes it different
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One language, back to front.&lt;/strong&gt; Components, handlers, and the server all live in Fitz. Your &lt;code&gt;type User { ... }&lt;/code&gt; is the same on both sides — no duplicated DTOs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero JavaScript build.&lt;/strong&gt; No &lt;code&gt;npm install&lt;/code&gt;, no bundler, no &lt;code&gt;node_modules&lt;/code&gt;. The &lt;code&gt;fitz&lt;/code&gt; binary compiles everything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Type-checked templates.&lt;/strong&gt; The template has its own checker: a prop of the wrong type, an &lt;code&gt;@event&lt;/code&gt; bound to a handler that doesn't exist, a &lt;code&gt;&amp;lt;slot slot="x"&amp;gt;&lt;/code&gt; with no &lt;code&gt;&amp;lt;slot name="x"&amp;gt;&lt;/code&gt; — all caught at compile time, pointing at the &lt;code&gt;.fitzv&lt;/code&gt; and the line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual-target: SSR &lt;em&gt;or&lt;/em&gt; WASM from one source.&lt;/strong&gt; Server-rendered for shared, DB-driven, multi-user state; client-WASM for local, zero-round-trip, offline widgets. Same component model either way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A packaged UI library.&lt;/strong&gt; &lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt; ships drop-in components (Badge, Card, DataGrid, Pager, Input, Select, Tabs, Stepper, TreeView, …), themed through &lt;code&gt;--flv-*&lt;/code&gt; design tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a &lt;strong&gt;&lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/" rel="noopener noreferrer"&gt;live component gallery&lt;/a&gt;&lt;/strong&gt; — real components compiled to WebAssembly, running in your browser, no server. Click around.&lt;/p&gt;

&lt;h2&gt;
  
  
  By the numbers
&lt;/h2&gt;

&lt;p&gt;These are measured payload/build numbers from the flagship admin app and the gallery — not synthetic throughput claims:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A full server-rendered dashboard page ships ~1.1 KB of JavaScript&lt;/strong&gt; (3 tiny inline &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; blocks: theme-before-paint + the WebSocket live client), &lt;strong&gt;zero external &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt;, and zero build step.&lt;/strong&gt; The page HTML is ~38 KB and that's &lt;em&gt;the whole thing&lt;/em&gt; — there's no framework runtime to download first. Compare that to an SPA, where the framework runtime alone is ~40 KB (Svelte-ish) to ~140 KB (React + ReactDOM) &lt;em&gt;before&lt;/em&gt; your code and your hydration payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A client-WASM component is ~12 KB gzipped&lt;/strong&gt; — the &lt;em&gt;entire&lt;/em&gt; component, not "your code on top of a 100 KB runtime." The counter demo is 11.4 KB gzipped; the full 12-widget composed gallery is ~44 KB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero &lt;code&gt;node_modules&lt;/code&gt;, zero bundler config.&lt;/strong&gt; The &lt;code&gt;fitz&lt;/code&gt; binary is the whole toolchain. &lt;code&gt;git clone&lt;/code&gt; → &lt;code&gt;fitz run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native binary ≈ 9× faster per interaction than the interpreter&lt;/strong&gt; (&lt;code&gt;fitz run&lt;/code&gt; ↔ &lt;code&gt;fitz build&lt;/code&gt; render bit-for-bit identical), so you develop on the interpreter and ship the binary.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Cross-framework request-throughput benchmarks are honest future work — I'd rather ship measured payload numbers than hand-wave a load test.)&lt;/p&gt;

&lt;h2&gt;
  
  
  How it compares
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Fitz LiveViews&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Phoenix LiveView&lt;/th&gt;
&lt;th&gt;Hotwire (Turbo)&lt;/th&gt;
&lt;th&gt;React/Vue/Svelte (SPA)&lt;/th&gt;
&lt;th&gt;HTMX&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Real-time diff over WebSocket&lt;/td&gt;
&lt;td&gt;✅ built-in&lt;/td&gt;
&lt;td&gt;✅ built-in&lt;/td&gt;
&lt;td&gt;~ (Turbo Streams)&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;~ (extension)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript build step&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;required&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Languages in the stack&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1&lt;/strong&gt; (Fitz)&lt;/td&gt;
&lt;td&gt;1 (Elixir)&lt;/td&gt;
&lt;td&gt;2 (Ruby + JS)&lt;/td&gt;
&lt;td&gt;2+ (backend + JS/TS)&lt;/td&gt;
&lt;td&gt;1 backend + HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offline / client-only target&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;WASM, same source&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (JS)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type-checked templates&lt;/td&gt;
&lt;td&gt;✅ compiler&lt;/td&gt;
&lt;td&gt;~ (HEEx)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;~ (TS/JSX)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compiles to a native binary&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ (BEAM VM)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packaged UI component library&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;community&lt;/td&gt;
&lt;td&gt;community&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;huge ecosystem&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;community&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Where Fitz LiveViews is genuinely different: &lt;strong&gt;one language across the whole stack&lt;/strong&gt;, a &lt;strong&gt;dual SSR/WASM target from a single source&lt;/strong&gt;, &lt;strong&gt;compilation to a standalone native binary&lt;/strong&gt;, and &lt;strong&gt;compiler-checked templates&lt;/strong&gt;. Where it's honestly behind: it's new, so the ecosystem is small — React/Vue/Svelte have a decade of libraries and hiring pools. If that trade — a smaller ecosystem for a radically simpler stack — sounds right for what you're building, read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Install Fitz.&lt;/strong&gt; Fitz LiveViews is a library for the Fitz language, so you install Fitz first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://thegreekman76.github.io/fitz/install.sh | sh
fitz &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Fitz course walks through install / uninstall / updating in detail — see &lt;strong&gt;&lt;a href="https://thegreekman76.github.io/fitz/curso/m1-setup/c1-instalacion/" rel="noopener noreferrer"&gt;Fitz course · C1 — Installation&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Install the editor extensions.&lt;/strong&gt; Two VSCode extensions give you syntax highlighting, diagnostics, hover, and go-to-definition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fitz&lt;/strong&gt; — for &lt;code&gt;.fitz&lt;/code&gt; files (the language). Grab the &lt;code&gt;.vsix&lt;/code&gt; from the &lt;a href="https://github.com/Thegreekman76/fitz/releases" rel="noopener noreferrer"&gt;Fitz releases&lt;/a&gt; and &lt;code&gt;code --install-extension fitz-language-*.vsix&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fitz LiveViews&lt;/strong&gt; — for &lt;code&gt;.fitzv&lt;/code&gt; single-file components. Grab it from the &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/releases" rel="noopener noreferrer"&gt;fitz-liveviews releases&lt;/a&gt; and &lt;code&gt;code --install-extension fitz-liveviews-*.vsix&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Add the dependency.&lt;/strong&gt; In your project's &lt;code&gt;fitz.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;fitz_liveviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;git&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/Thegreekman76/fitz-liveviews"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole setup — no Node, no bundler, no &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not a toy — there's a flagship
&lt;/h2&gt;

&lt;p&gt;To prove the model on something real, there's a complete back-office &lt;strong&gt;admin panel&lt;/strong&gt; built entirely in Fitz + Fitz LiveViews: login (Argon2id + a signed JWT session cookie), a responsive shell (collapsible sidebar, mobile drawer down to 320px, ES/EN switch, light/dark/auto theme), a dashboard with real counts from Postgres, and two live CRUD screens (employees + departments) with search, filters, sorting, pagination, rich tabbed forms, multi-delete, group-by, per-row expand, and CSV export — all diffed over WebSockets, all internationalized, all in one &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every reusable piece of it is a packaged component in &lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt; — the library was &lt;em&gt;extracted&lt;/em&gt; from this app. It's the living reference.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo&lt;/strong&gt; — &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;github.com/Thegreekman76/fitz-liveviews&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs&lt;/strong&gt; — &lt;a href="https://thegreekman76.github.io/fitz-liveviews/" rel="noopener noreferrer"&gt;thegreekman76.github.io/fitz-liveviews&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live gallery&lt;/strong&gt; — &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/" rel="noopener noreferrer"&gt;/live&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Course&lt;/strong&gt; — a hands-on, chapter-by-chapter build (&lt;a href="https://thegreekman76.github.io/fitz-liveviews/course/" rel="noopener noreferrer"&gt;overview&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Client-WASM guide&lt;/strong&gt; — the offline-widget target (&lt;a href="https://thegreekman76.github.io/fitz-liveviews/client-wasm/" rel="noopener noreferrer"&gt;client-wasm&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's next in this series
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#2 — Your first component, twice.&lt;/strong&gt; A counter that renders server-side over a WebSocket &lt;em&gt;and&lt;/em&gt; compiles to WebAssembly — one source, two targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;#3 — Forms, payloads, and live inputs.&lt;/strong&gt; How events carry data, and how &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt; bind form values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;#4+ — Building the flagship.&lt;/strong&gt; A deep dive into the admin panel: cookie auth, live DataGrids over Postgres, the packaged UI library, i18n, and Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If real-time UI without a JavaScript build sounds good, star the &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt; and follow the series. Next up: the counter, twice.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Presentando Fitz LiveViews: UI en tiempo real en un solo lenguaje, sin build de JS</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Sat, 01 Aug 2026 12:02:38 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/presentando-fitz-liveviews-ui-en-tiempo-real-en-un-solo-lenguaje-sin-build-de-js-2147</link>
      <guid>https://dev.to/martin_palopoli/presentando-fitz-liveviews-ui-en-tiempo-real-en-un-solo-lenguaje-sin-build-de-js-2147</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;TL;DR — &lt;strong&gt;Fitz LiveViews&lt;/strong&gt; es un framework de UI en tiempo real para &lt;a href="https://github.com/Thegreekman76/fitz" rel="noopener noreferrer"&gt;Fitz&lt;/a&gt;, un lenguaje compilado y de tipado gradual donde HTTP, WebSockets, auth y un ORM son parte de la sintaxis. Escribís componentes de un solo archivo (&lt;code&gt;.fitzv&lt;/code&gt;) con &lt;code&gt;state&lt;/code&gt; / &lt;code&gt;event&lt;/code&gt; / &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt;, y el servidor renderiza HTML, lo diffea y parchea el browser por WebSocket — &lt;strong&gt;sin paso de build de JavaScript, sin framework de cliente&lt;/strong&gt;. El mismo &lt;code&gt;.fitzv&lt;/code&gt; puede &lt;em&gt;además&lt;/em&gt; compilar a WebAssembly para widgets offline sin round-trip. Ya hay una galería de componentes en vivo, un curso, y una app flagship completa (un panel de administración con auth + Postgres + Docker) construida con esto. &lt;strong&gt;Repo&lt;/strong&gt;: &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;github.com/Thegreekman76/fitz-liveviews&lt;/a&gt; · &lt;strong&gt;Docs&lt;/strong&gt;: &lt;a href="https://thegreekman76.github.io/fitz-liveviews/" rel="noopener noreferrer"&gt;thegreekman76.github.io/fitz-liveviews&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Este es el primer post de la serie &lt;strong&gt;FitzLiveViews&lt;/strong&gt;. Arranco con el pitch y el setup; los siguientes construyen cosas.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema
&lt;/h2&gt;

&lt;p&gt;Armar una UI web moderna normalmente implica dos lenguajes, dos sistemas de tipos, y un pipeline de build: un backend (Python / Node / Go) más un framework de frontend (React / Vue / Svelte) más su toolchain (Vite / Webpack / Babel). Duplicás tus tipos de un lado al otro del cable, mantenés dos modelos mentales en sync, y &lt;code&gt;node_modules&lt;/code&gt; desarrolla personalidad propia.&lt;/p&gt;

&lt;p&gt;Phoenix LiveView (Elixir) mostró que hay otra forma: renderizar en el servidor, empujar diffs por WebSocket, y dejar que el browser quede tonto. Sin framework de cliente, sin API que escribir a mano, sin la danza de serializar JSON. Fitz LiveViews trae ese modelo a Fitz — y suma una vuelta de tuerca: el &lt;em&gt;mismo componente&lt;/em&gt; puede además compilar a WebAssembly cuando querés interactividad puramente client-side y offline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo se ve Fitz LiveViews
&lt;/h2&gt;

&lt;p&gt;Un componente es un solo archivo &lt;code&gt;.fitzv&lt;/code&gt; — state, event handlers y template, como Vue o Svelte:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;Counter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;decrement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="nf"&gt;reset&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;counter-app&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;increment&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;decrement&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reset&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Reset&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/template&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En el target &lt;strong&gt;server-rendered&lt;/strong&gt;, el framework registra el componente, manda el HTML inicial y — en cada evento — recomputa el HTML, lo diffea, y manda solo los parches por WebSocket. El browser los aplica. Nunca escribís JavaScript de cliente, nunca definís una API, nunca serializás un payload a mano.&lt;/p&gt;

&lt;p&gt;En el target &lt;strong&gt;client-WASM&lt;/strong&gt; (&lt;code&gt;fitz build --target wasm-client&lt;/code&gt;), el &lt;em&gt;mismo archivo&lt;/em&gt; compila a un bundle &lt;code&gt;wasm-bindgen&lt;/code&gt; + &lt;code&gt;web-sys&lt;/code&gt; que monta y muta el DOM directo — offline, sin servidor. Un componente pesa ~12 KB gzipped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué lo hace distinto
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Un lenguaje, del back al front.&lt;/strong&gt; Componentes, handlers y el servidor viven todos en Fitz. Tu &lt;code&gt;type User { ... }&lt;/code&gt; es el mismo de los dos lados — sin DTOs duplicados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cero build de JavaScript.&lt;/strong&gt; Sin &lt;code&gt;npm install&lt;/code&gt;, sin bundler, sin &lt;code&gt;node_modules&lt;/code&gt;. El binario &lt;code&gt;fitz&lt;/code&gt; compila todo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templates con chequeo de tipos.&lt;/strong&gt; El template tiene su propio checker: un prop del tipo equivocado, un &lt;code&gt;@event&lt;/code&gt; bindeado a un handler que no existe, un &lt;code&gt;&amp;lt;slot slot="x"&amp;gt;&lt;/code&gt; sin &lt;code&gt;&amp;lt;slot name="x"&amp;gt;&lt;/code&gt; — todo se caza en compile-time, apuntando al &lt;code&gt;.fitzv&lt;/code&gt; y a la línea.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dual-target: SSR &lt;em&gt;o&lt;/em&gt; WASM del mismo source.&lt;/strong&gt; Server-rendered para estado compartido, DB-driven, multi-usuario; client-WASM para widgets locales, offline, sin round-trip. El mismo modelo de componentes en los dos casos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Una librería de UI empaquetada.&lt;/strong&gt; &lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt; trae componentes drop-in (Badge, Card, DataGrid, Pager, Input, Select, Tabs, Stepper, TreeView, …), tematizados con design tokens &lt;code&gt;--flv-*&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hay una &lt;strong&gt;&lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/" rel="noopener noreferrer"&gt;galería de componentes en vivo&lt;/a&gt;&lt;/strong&gt; — componentes reales compilados a WebAssembly, corriendo en tu browser, sin servidor. Jugá un rato.&lt;/p&gt;

&lt;h2&gt;
  
  
  En números
&lt;/h2&gt;

&lt;p&gt;Estos son números medidos de payload/build de la app flagship y la galería — no claims sintéticos de throughput:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Una página completa del dashboard server-rendered envía ~1.1 KB de JavaScript&lt;/strong&gt; (3 bloques &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; inline chiquitos: el theme-antes-del-paint + el cliente WebSocket en vivo), &lt;strong&gt;cero &lt;code&gt;&amp;lt;script src&amp;gt;&lt;/code&gt; externos, y cero paso de build.&lt;/strong&gt; El HTML de la página son ~38 KB y eso es &lt;em&gt;todo&lt;/em&gt; — no hay runtime de framework que bajar primero. Compará con un SPA, donde el runtime del framework solo son ~40 KB (estilo Svelte) a ~140 KB (React + ReactDOM) &lt;em&gt;antes&lt;/em&gt; de tu código y tu payload de hydration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Un componente client-WASM son ~12 KB gzipped&lt;/strong&gt; — el componente &lt;em&gt;entero&lt;/em&gt;, no "tu código arriba de un runtime de 100 KB". El demo del counter son 11.4 KB gzipped; la galería completa de 12 widgets compuestos son ~44 KB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cero &lt;code&gt;node_modules&lt;/code&gt;, cero config de bundler.&lt;/strong&gt; El binario &lt;code&gt;fitz&lt;/code&gt; es todo el toolchain. &lt;code&gt;git clone&lt;/code&gt; → &lt;code&gt;fitz run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Binario nativo ≈ 9× más rápido por interacción que el intérprete&lt;/strong&gt; (&lt;code&gt;fitz run&lt;/code&gt; ↔ &lt;code&gt;fitz build&lt;/code&gt; renderizan bit-a-bit idéntico), así que desarrollás sobre el intérprete y shipeás el binario.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;(Los benchmarks de throughput de requests cross-framework son trabajo futuro honesto — prefiero shipear números de payload medidos que agitar las manos con un load test.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo se compara
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Fitz LiveViews&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;Phoenix LiveView&lt;/th&gt;
&lt;th&gt;Hotwire (Turbo)&lt;/th&gt;
&lt;th&gt;React/Vue/Svelte (SPA)&lt;/th&gt;
&lt;th&gt;HTMX&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Diff en tiempo real por WebSocket&lt;/td&gt;
&lt;td&gt;✅ built-in&lt;/td&gt;
&lt;td&gt;✅ built-in&lt;/td&gt;
&lt;td&gt;~ (Turbo Streams)&lt;/td&gt;
&lt;td&gt;manual&lt;/td&gt;
&lt;td&gt;~ (extensión)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paso de build de JavaScript&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ninguno&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ninguno&lt;/td&gt;
&lt;td&gt;ninguno&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;requerido&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ninguno&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lenguajes en el stack&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;1&lt;/strong&gt; (Fitz)&lt;/td&gt;
&lt;td&gt;1 (Elixir)&lt;/td&gt;
&lt;td&gt;2 (Ruby + JS)&lt;/td&gt;
&lt;td&gt;2+ (backend + JS/TS)&lt;/td&gt;
&lt;td&gt;1 backend + HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Target offline / solo-cliente&lt;/td&gt;
&lt;td&gt;✅ &lt;strong&gt;WASM, mismo source&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (JS)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Templates con chequeo de tipos&lt;/td&gt;
&lt;td&gt;✅ compilador&lt;/td&gt;
&lt;td&gt;~ (HEEx)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;~ (TS/JSX)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compila a binario nativo&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;❌ (VM BEAM)&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Librería de componentes UI empaquetada&lt;/td&gt;
&lt;td&gt;✅ &lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;comunidad&lt;/td&gt;
&lt;td&gt;comunidad&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;ecosistema enorme&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;comunidad&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Donde Fitz LiveViews es genuinamente distinto: &lt;strong&gt;un lenguaje en todo el stack&lt;/strong&gt;, un &lt;strong&gt;target dual SSR/WASM del mismo source&lt;/strong&gt;, &lt;strong&gt;compilación a binario nativo standalone&lt;/strong&gt;, y &lt;strong&gt;templates chequeados por el compilador&lt;/strong&gt;. Donde está honestamente atrás: es nuevo, así que el ecosistema es chico — React/Vue/Svelte tienen una década de librerías y de gente que las sabe. Si ese trade — un ecosistema más chico a cambio de un stack radicalmente más simple — suena bien para lo que estás construyendo, seguí leyendo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo empezar
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Instalá Fitz.&lt;/strong&gt; Fitz LiveViews es una librería del lenguaje Fitz, así que primero instalás Fitz:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSf&lt;/span&gt; https://thegreekman76.github.io/fitz/install.sh | sh
fitz &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El curso de Fitz recorre install / desinstalación / actualización en detalle — mirá &lt;strong&gt;&lt;a href="https://thegreekman76.github.io/fitz/curso/m1-setup/c1-instalacion/" rel="noopener noreferrer"&gt;Curso de Fitz · C1 — Instalación&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Instalá las extensiones del editor.&lt;/strong&gt; Dos extensiones de VSCode te dan syntax highlighting, diagnostics, hover y go-to-definition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fitz&lt;/strong&gt; — para archivos &lt;code&gt;.fitz&lt;/code&gt; (el lenguaje). Bajá el &lt;code&gt;.vsix&lt;/code&gt; de los &lt;a href="https://github.com/Thegreekman76/fitz/releases" rel="noopener noreferrer"&gt;releases de Fitz&lt;/a&gt; y &lt;code&gt;code --install-extension fitz-language-*.vsix&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fitz LiveViews&lt;/strong&gt; — para componentes &lt;code&gt;.fitzv&lt;/code&gt;. Bajalo de los &lt;a href="https://github.com/Thegreekman76/fitz-liveviews/releases" rel="noopener noreferrer"&gt;releases de fitz-liveviews&lt;/a&gt; y &lt;code&gt;code --install-extension fitz-liveviews-*.vsix&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Agregá la dependencia.&lt;/strong&gt; En el &lt;code&gt;fitz.toml&lt;/code&gt; de tu proyecto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[dependencies]&lt;/span&gt;
&lt;span class="py"&gt;fitz_liveviews&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;git&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://github.com/Thegreekman76/fitz-liveviews"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ese es todo el setup — sin Node, sin bundler, sin &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  No es un juguete — hay un flagship
&lt;/h2&gt;

&lt;p&gt;Para probar el modelo en algo real, hay un &lt;strong&gt;panel de administración&lt;/strong&gt; de back-office completo, construido enteramente en Fitz + Fitz LiveViews: login (Argon2id + cookie de sesión con JWT firmado), un shell responsive (sidebar colapsable, drawer mobile hasta 320px, switch ES/EN, tema light/dark/auto), un dashboard con counts reales de Postgres, y dos pantallas CRUD en vivo (empleados + departamentos) con búsqueda, filtros, ordenamiento, paginación, forms ricos con tabs, multi-delete, group-by, expand por fila, y export a CSV — todo diffeado por WebSockets, todo internacionalizado, todo en un &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Cada pieza reusable es un componente empaquetado en &lt;code&gt;fitz_liveviews.ui.*&lt;/code&gt; — la librería se &lt;em&gt;extrajo&lt;/em&gt; de esta app. Es la referencia viva.&lt;/p&gt;

&lt;h2&gt;
  
  
  A dónde ir
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo&lt;/strong&gt; — &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;github.com/Thegreekman76/fitz-liveviews&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docs&lt;/strong&gt; — &lt;a href="https://thegreekman76.github.io/fitz-liveviews/" rel="noopener noreferrer"&gt;thegreekman76.github.io/fitz-liveviews&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Galería en vivo&lt;/strong&gt; — &lt;a href="https://thegreekman76.github.io/fitz-liveviews/live/" rel="noopener noreferrer"&gt;/live&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Curso&lt;/strong&gt; — una construcción práctica, capítulo a capítulo (&lt;a href="https://thegreekman76.github.io/fitz-liveviews/course/" rel="noopener noreferrer"&gt;overview&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guía Client-WASM&lt;/strong&gt; — el target de widgets offline (&lt;a href="https://thegreekman76.github.io/fitz-liveviews/client-wasm/" rel="noopener noreferrer"&gt;client-wasm&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Qué viene en la serie
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;#2 — Tu primer componente, dos veces.&lt;/strong&gt; Un counter que renderiza server-side por WebSocket &lt;em&gt;y&lt;/em&gt; compila a WebAssembly — un source, dos targets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;#3 — Forms, payloads e inputs en vivo.&lt;/strong&gt; Cómo los eventos llevan data, y cómo &lt;code&gt;@input&lt;/code&gt; / &lt;code&gt;@change&lt;/code&gt; bindean valores de formulario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;#4+ — Construyendo el flagship.&lt;/strong&gt; Un deep dive al panel de administración: auth por cookie, DataGrids en vivo sobre Postgres, la librería de UI empaquetada, i18n, y Docker.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Si UI en tiempo real sin build de JavaScript te suena bien, dale una estrella al &lt;a href="https://github.com/Thegreekman76/fitz-liveviews" rel="noopener noreferrer"&gt;repo&lt;/a&gt; y seguí la serie. Lo próximo: el counter, dos veces.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>rust</category>
      <category>opensource</category>
      <category>frontend</category>
    </item>
    <item>
      <title>SMTP outbound as a first-class citizen: send emails without pip install yagmail</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Mon, 13 Jul 2026 17:43:42 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/smtp-outbound-as-a-first-class-citizen-send-emails-without-pip-install-yagmail-46dd</link>
      <guid>https://dev.to/martin_palopoli/smtp-outbound-as-a-first-class-citizen-send-emails-without-pip-install-yagmail-46dd</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Notifications, password resets, magic links, alerts. Every language solves SMTP outbound with an external library. In Fitz, &lt;code&gt;smtp.send(opts)&lt;/code&gt; is a language builtin. Async from day one. Bit-for-bit parity between &lt;code&gt;fitz run&lt;/code&gt; and &lt;code&gt;fitz build&lt;/code&gt;. No &lt;code&gt;pip install yagmail&lt;/code&gt; / &lt;code&gt;npm install nodemailer&lt;/code&gt; / &lt;code&gt;cargo add lettre&lt;/code&gt; / Maven &lt;code&gt;JavaMail&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The detail that gets forgotten
&lt;/h2&gt;

&lt;p&gt;Any serious application eventually needs to send emails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notifications when a service goes down (incidents → on-call email).&lt;/li&gt;
&lt;li&gt;Password reset / magic-link auth (unique link to the user's email).&lt;/li&gt;
&lt;li&gt;Welcome email after a signup.&lt;/li&gt;
&lt;li&gt;Daily / weekly digest reports dispatched from a cron job.&lt;/li&gt;
&lt;li&gt;Alerts when a nightly job fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And every language solves this with an external library.&lt;/p&gt;

&lt;p&gt;We hit the gap while building &lt;strong&gt;fitzwatch&lt;/strong&gt; (open-source status page written in pure Fitz): the heart of the product alerts on-call when a monitor goes down. Without native SMTP, the only option was webhook → n8n/zapier → email. Two external systems just to send one email.&lt;/p&gt;

&lt;p&gt;Today, the 8 sub-blocks of the mini-release are in &lt;code&gt;main&lt;/code&gt;. One line — &lt;code&gt;smtp.send({...}).await?&lt;/code&gt; — and the email goes out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The typical Python stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;yagmail
&lt;span class="c"&gt;# or use stdlib smtplib (RFC 5321 by hand)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# With yagmail (friendly syntax):
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yagmail&lt;/span&gt;

&lt;span class="n"&gt;yag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yagmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;yag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dest@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# With stdlib smtplib (low-level):
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.mime.text&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEText&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.mime.multipart&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEMultipart&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MIMEMultipart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alternative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;From&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;To&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dest@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;p&amp;gt;Body HTML&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smtp.gmail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;starttls&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two options, both require reading the RFC or learning the yagmail API.&lt;/p&gt;

&lt;h2&gt;
  
  
  The typical JS/Node stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;nodemailer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nodemailer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smtp.gmail.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dest@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Subject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Body text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;Body HTML&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One library, well-designed, but &lt;code&gt;npm install&lt;/code&gt; brings ~50 transitive packages to &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The typical Rust stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add lettre
cargo add tokio &lt;span class="nt"&gt;--features&lt;/span&gt; full
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;lettre&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;message&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nn"&gt;header&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mailbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MultiPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SinglePart&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;lettre&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;authentication&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;lettre&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;AsyncSmtpTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AsyncTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tokio1Executor&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user@example.com"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mailer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AsyncSmtpTransport&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Tokio1Executor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;starttls_relay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"smtp.gmail.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user@example.com"&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mailbox&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;.to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dest@example.com"&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mailbox&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;.subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Subject"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.multipart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MultiPart&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;alternative&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.singlepart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SinglePart&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TEXT_PLAIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Body text"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
        &lt;span class="nf"&gt;.singlepart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SinglePart&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TEXT_HTML&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;p&amp;gt;Body HTML&amp;lt;/p&amp;gt;"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functional, but verbose. 25 lines to send ONE email.&lt;/p&gt;

&lt;h2&gt;
  
  
  The typical Java stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.sun.mail&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;jakarta.mail&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.0.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then the canonical recipe of &lt;code&gt;Properties&lt;/code&gt; + &lt;code&gt;Session&lt;/code&gt; + &lt;code&gt;MimeMessage&lt;/code&gt; + &lt;code&gt;Transport.send(...)&lt;/code&gt; that probably adds 40 lines.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fitz stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let r = smtp.send({
    "to": "dest@example.com",
    "from": "user@example.com",
    "subject": "Subject",
    "body_text": "Body text",
    "body_html": "&amp;lt;p&amp;gt;Body HTML&amp;lt;/p&amp;gt;",
}).await?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;smtp&lt;/code&gt; is a language builtin. No &lt;code&gt;pip install&lt;/code&gt;/&lt;code&gt;npm install&lt;/code&gt;/&lt;code&gt;cargo add&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;body_text&lt;/code&gt; + &lt;code&gt;body_html&lt;/code&gt; together → automatic multipart/alternative.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.await?&lt;/code&gt; propagates errors as &lt;code&gt;Result::Err(Str)&lt;/code&gt;. The compiler enforces handling.&lt;/li&gt;
&lt;li&gt;Config (host, port, user, password, TLS) is read from env vars at first send. Same convention any app in Kubernetes/Docker expects.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;r.delivered&lt;/code&gt;, &lt;code&gt;r.message_id&lt;/code&gt;, and &lt;code&gt;r.duration_ms&lt;/code&gt; come out typed as &lt;code&gt;Bool&lt;/code&gt;/&lt;code&gt;Str&lt;/code&gt;/&lt;code&gt;Int&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What &lt;code&gt;smtp.send&lt;/code&gt; guarantees you
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Zero external deps
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;fitz&lt;/code&gt; binary ships with &lt;code&gt;lettre 0.11&lt;/code&gt; statically linked inside. When you &lt;code&gt;fitz build&lt;/code&gt;, the result is ONE executable of ~10 MB containing the SMTP client, TLS via rustls, no openssl on the target host.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ldd ./my-app
&lt;span class="c"&gt;# linux-vdso.so.1&lt;/span&gt;
&lt;span class="c"&gt;# libgcc_s.so.1&lt;/span&gt;
&lt;span class="c"&gt;# libc.so.6&lt;/span&gt;
&lt;span class="c"&gt;# (no smtp libs, no openssl)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Bit-for-bit parity &lt;code&gt;fitz run&lt;/code&gt; ↔ &lt;code&gt;fitz build&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The interpreter (&lt;code&gt;fitz run&lt;/code&gt;) and codegen to Rust (&lt;code&gt;fitz build&lt;/code&gt;) emit the same wire SMTP. Same handshake, same SCRAM auth, same Message-ID. Bug in one path = bug in the other. E2E tests validate this on every commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Async from day one
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;smtp.send(...)&lt;/code&gt; returns &lt;code&gt;Future&amp;lt;Result&amp;lt;SmtpResult&amp;gt;&amp;gt;&lt;/code&gt;. Integrates naturally with the rest of the stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@background
async fn send_welcome(email: Str) -&amp;gt; Null {
    let _ = smtp.send({
        "to": email,
        "subject": "Welcome",
        "body": "Hi, thanks for joining.",
    }).await
    return null
}

@post("/signup")
fn signup(input: SignupInput) {
    // ... create the user in the DB ...
    let _ = spawn(send_welcome(input.email))
    return 201 { "id": new_user_id }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler returns 201 to the client &lt;strong&gt;immediately&lt;/strong&gt;. The email goes out in another tokio task. If the SMTP server takes 2 seconds, the client doesn't care — it already responded 2 seconds ago.&lt;/p&gt;

&lt;p&gt;And with &lt;code&gt;@cron&lt;/code&gt; you build digests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@cron("0 0 9 * * *")  // every day at 09:00
async fn daily_digest() -&amp;gt; Null {
    let r = smtp.send({
        "to": "team@example.com",
        "subject": "Daily digest",
        "body_html": "&amp;lt;p&amp;gt;Today: ...&amp;lt;/p&amp;gt;",
    }).await
    match r {
        Ok(_) =&amp;gt; log.info("digest.sent"),
        Err(e) =&amp;gt; log.error("digest.failed", error: e),
    }
    return null
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; as error model
&lt;/h3&gt;

&lt;p&gt;Transport errors (DNS, auth, TLS, server reject) come as &lt;code&gt;Result::Err(Str)&lt;/code&gt; with &lt;code&gt;"smtp: "&lt;/code&gt; prefix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;match smtp.send(opts).await {
    Ok(r) =&amp;gt; log.info("smtp.delivered", message_id: r.message_id),
    Err(e) =&amp;gt; {
        // The prefix lets you classify:
        // "smtp: server rejected mail: ..."     → 5xx from server
        // "smtp: transient error: ..."          → 4xx temporary
        // "smtp: client error: ..."             → TLS/auth fail
        // "smtp: invalid `to` address ..."      → parse error
        log.warn("smtp.failed", error: e)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The static checker enforces handling. If your fn returns &lt;code&gt;Result&amp;lt;...&amp;gt;&lt;/code&gt;, you can propagate with &lt;code&gt;?&lt;/code&gt;. If not, the checker demands &lt;code&gt;match&lt;/code&gt;. Zero runtime exceptions.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Magic-link auth in 30 lines
&lt;/h3&gt;

&lt;p&gt;The showcase case of Fitz's first-class web stack: HTTP server-side + auth with &lt;code&gt;jwt.encode&lt;/code&gt; + SMTP outbound, all combined in one binary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type EmailRequest {
    email: Str
}

@post("/auth/magic-link")
async fn magic_link(input: EmailRequest) -&amp;gt; Result&amp;lt;Str&amp;gt; {
    let payload = { "email": input.email }
    let token = jwt.encode(payload, "secret")
    let link = "https://app.example.com/verify?t={token}"
    let r = smtp.send({
        "to": input.email,
        "subject": "Your login link",
        "body_text": "Click here:\n{link}\n\nExpires in 5 min.",
        "body_html": "&amp;lt;p&amp;gt;Click &amp;lt;a href=\"{link}\"&amp;gt;here&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;",
    }).await?
    return Ok(r.message_id)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No Auth0, no Supabase, no Stripe webhooks. One binary, one port, one command to deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local setup with MailHog
&lt;/h2&gt;

&lt;p&gt;For dev you don't want to send real emails. &lt;a href="https://github.com/mailhog/MailHog" rel="noopener noreferrer"&gt;MailHog&lt;/a&gt; is a fake SMTP server with a web UI that retains emails without forwarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; mailhog &lt;span class="nt"&gt;-p&lt;/span&gt; 1025:1025 &lt;span class="nt"&gt;-p&lt;/span&gt; 8025:8025 mailhog/mailhog
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SMTP_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;localhost
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SMTP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1025
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SMTP_TLS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none
fitz run app.fitz
&lt;span class="c"&gt;# Then open http://localhost:8025 in the browser.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production you change the env vars to Gmail/SES/SendGrid/Mailgun/whatever — your Fitz code doesn't change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade-offs (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No attachments yet&lt;/strong&gt;. The &lt;code&gt;attachments&lt;/code&gt; key is rejected with a clear error. Workaround: bundle attachments via S3/object storage + link in the body. The cleanest pattern for serious production anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No programmatic &lt;code&gt;smtp.configure(...)&lt;/code&gt;&lt;/strong&gt;. Config only comes from env vars today. If you need multi-tenant with a different SMTP per tenant, that's pending as post-MVP debt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No built-in retry&lt;/strong&gt;. If &lt;code&gt;smtp.send&lt;/code&gt; fails, the caller decides what to do. The canonical pattern for production is outbox table + &lt;code&gt;@cron&lt;/code&gt; with retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is NOT FUD — these are explicit MVP decisions. All 3 are refinable if real demand shows up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;smtp.send&lt;/code&gt; isn't "another easier library". It's &lt;strong&gt;the same mental model&lt;/strong&gt; you already use elsewhere in Fitz: &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; as return type, &lt;code&gt;?&lt;/code&gt; to propagate, &lt;code&gt;.await&lt;/code&gt; for async, &lt;code&gt;@background + spawn&lt;/code&gt; for fire-and-forget.&lt;/p&gt;

&lt;p&gt;If you already know how to chain &lt;code&gt;http.get(...).await?&lt;/code&gt; or &lt;code&gt;db.query(...).await?&lt;/code&gt;, you already know how to send emails. Zero new API to learn.&lt;/p&gt;

&lt;p&gt;And when fitzwatch resumes, the line that was pending to notify incidents goes from "webhook to n8n + translation to SMTP" to &lt;code&gt;smtp.send({...}).await?&lt;/code&gt;. One line. That's Fitz.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Try it&lt;/strong&gt;: &lt;code&gt;fitz&lt;/code&gt; version &lt;code&gt;0.18.0+&lt;/code&gt; (already in &lt;a href="https://github.com/Thegreekman76/fitz/releases/latest" rel="noopener noreferrer"&gt;GitHub releases&lt;/a&gt;). Exhaustive chapter + 3 runnable examples against MailHog in the &lt;a href="https://thegreekman76.github.io/fitz/guide/#17-http-nativo" rel="noopener noreferrer"&gt;"SMTP outbound" subsection of chapter 17 of the guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stay tuned&lt;/strong&gt;: the Fitz series publishes on &lt;a href="https://dev.to/"&gt;dev.to&lt;/a&gt; every time we close a major mini-release. The next one will cover a concrete feature detected during fitzwatch development.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>email</category>
    </item>
    <item>
      <title>SMTP outbound como ciudadano de primera clase: mandá emails sin pip install yagmail</title>
      <dc:creator>Martin Palopoli</dc:creator>
      <pubDate>Mon, 13 Jul 2026 17:43:20 +0000</pubDate>
      <link>https://dev.to/martin_palopoli/smtp-outbound-como-ciudadano-de-primera-clase-manda-emails-sin-pip-install-yagmail-1nfa</link>
      <guid>https://dev.to/martin_palopoli/smtp-outbound-como-ciudadano-de-primera-clase-manda-emails-sin-pip-install-yagmail-1nfa</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Notificaciones, password resets, magic links, alerts. Todos los lenguajes resuelven SMTP outbound con una librería externa. En Fitz, &lt;code&gt;smtp.send(opts)&lt;/code&gt; es builtin del lenguaje. Async desde día uno. Paridad bit-a-bit &lt;code&gt;fitz run&lt;/code&gt; ↔ &lt;code&gt;fitz build&lt;/code&gt;. Sin &lt;code&gt;pip install yagmail&lt;/code&gt; / &lt;code&gt;npm install nodemailer&lt;/code&gt; / &lt;code&gt;cargo add lettre&lt;/code&gt; / Maven &lt;code&gt;JavaMail&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  El detalle que se olvida
&lt;/h2&gt;

&lt;p&gt;Cualquier aplicación seria termina necesitando mandar mails:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Notificación cuando se cae un servicio (incidents → email al on-call).&lt;/li&gt;
&lt;li&gt;Password reset / magic-link auth (link único al email del user).&lt;/li&gt;
&lt;li&gt;Welcome email después de un signup.&lt;/li&gt;
&lt;li&gt;Reportes diarios / weekly digests despachados desde un cron job.&lt;/li&gt;
&lt;li&gt;Alertas de jobs nocturnos que fallan.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y todos los lenguajes lo resuelven con una librería externa.&lt;/p&gt;

&lt;p&gt;Detectamos el gap construyendo &lt;strong&gt;fitzwatch&lt;/strong&gt; (status page open-source escrito en Fitz puro): el corazón del producto avisa al on-call cuando un monitor cae. Sin SMTP nativo, la única opción era webhook → n8n/zapier → email. Dos sistemas externos para mandar un email.&lt;/p&gt;

&lt;p&gt;Hoy, los 8 sub-bloques de la mini-tanda están en &lt;code&gt;main&lt;/code&gt;. Una línea — &lt;code&gt;smtp.send({...}).await?&lt;/code&gt; — y el email sale.&lt;/p&gt;

&lt;h2&gt;
  
  
  El stack típico de Python
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;yagmail
&lt;span class="c"&gt;# o usar smtplib del stdlib (RFC 5321 a mano)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Con yagmail (sintaxis amigable):
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;yagmail&lt;/span&gt;

&lt;span class="n"&gt;yag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yagmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;yag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dest@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Con smtplib stdlib (bajo nivel):
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.mime.text&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEText&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.mime.multipart&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MIMEMultipart&lt;/span&gt;

&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MIMEMultipart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alternative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Subject&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;From&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;To&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dest@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Body text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MIMEText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;p&amp;gt;Body HTML&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;smtplib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SMTP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;smtp.gmail.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;starttls&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dos opciones, ambas requieren leerte la RFC o aprender la API de yagmail.&lt;/p&gt;

&lt;h2&gt;
  
  
  El stack típico de JS/Node
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;nodemailer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nodemailer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;transporter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nodemailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createTransport&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;smtp.gmail.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;secure&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;transporter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMail&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;dest@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Subject&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Body text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;p&amp;gt;Body HTML&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Una sola lib, bien diseñada, pero &lt;code&gt;npm install&lt;/code&gt; agrega ~50 paquetes transitivos al &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  El stack típico de Rust
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo add lettre
cargo add tokio &lt;span class="nt"&gt;--features&lt;/span&gt; full
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;lettre&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;message&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nn"&gt;header&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mailbox&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MultiPart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SinglePart&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;lettre&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;smtp&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;authentication&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;lettre&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;AsyncSmtpTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AsyncTransport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tokio1Executor&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Credentials&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user@example.com"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="nf"&gt;.into&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;mailer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;AsyncSmtpTransport&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Tokio1Executor&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;starttls_relay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"smtp.gmail.com"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.port&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;587&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.credentials&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;creds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user@example.com"&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mailbox&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;.to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dest@example.com"&lt;/span&gt;&lt;span class="py"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mailbox&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;.subject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Subject"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.multipart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;MultiPart&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;alternative&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.singlepart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SinglePart&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TEXT_PLAIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Body text"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
        &lt;span class="nf"&gt;.singlepart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;SinglePart&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;ContentType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;TEXT_HTML&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.body&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"&amp;lt;p&amp;gt;Body HTML&amp;lt;/p&amp;gt;"&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="n"&gt;mailer&lt;/span&gt;&lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Funcional, pero verboso. 25 líneas para mandar UN mail.&lt;/p&gt;

&lt;h2&gt;
  
  
  El stack típico de Java
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.sun.mail&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;jakarta.mail&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.0.1&lt;span class="nt"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y después la receta canónica de &lt;code&gt;Properties&lt;/code&gt; + &lt;code&gt;Session&lt;/code&gt; + &lt;code&gt;MimeMessage&lt;/code&gt; + &lt;code&gt;Transport.send(...)&lt;/code&gt; que probablemente sumás 40 líneas.&lt;/p&gt;

&lt;h2&gt;
  
  
  El stack de Fitz
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;let r = smtp.send({
    "to": "dest@example.com",
    "from": "user@example.com",
    "subject": "Subject",
    "body_text": "Body text",
    "body_html": "&amp;lt;p&amp;gt;Body HTML&amp;lt;/p&amp;gt;",
}).await?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Que es esto.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;smtp&lt;/code&gt; es builtin del lenguaje. No hay &lt;code&gt;pip install&lt;/code&gt;/&lt;code&gt;npm install&lt;/code&gt;/&lt;code&gt;cargo add&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;body_text&lt;/code&gt; + &lt;code&gt;body_html&lt;/code&gt; juntos → multipart/alternative automático.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.await?&lt;/code&gt; propaga errores como &lt;code&gt;Result::Err(Str)&lt;/code&gt;. El compilador exige manejo.&lt;/li&gt;
&lt;li&gt;Config (host, port, user, password, TLS) se lee de env vars al primer send. Lo mismo que cualquier app en Kubernetes/Docker espera.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;r.delivered&lt;/code&gt;, &lt;code&gt;r.message_id&lt;/code&gt; y &lt;code&gt;r.duration_ms&lt;/code&gt; salen tipados como &lt;code&gt;Bool&lt;/code&gt;/&lt;code&gt;Str&lt;/code&gt;/&lt;code&gt;Int&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que &lt;code&gt;smtp.send&lt;/code&gt; te garantiza
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Cero deps externas
&lt;/h3&gt;

&lt;p&gt;El binario &lt;code&gt;fitz&lt;/code&gt; ya viene con &lt;code&gt;lettre 0.11&lt;/code&gt; linkeado estático adentro. Cuando hacés &lt;code&gt;fitz build&lt;/code&gt;, el resultado es UN ejecutable de ~10 MB que tiene cliente SMTP, TLS via rustls, sin openssl en el host destino.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ldd ./mi-app
&lt;span class="c"&gt;# linux-vdso.so.1&lt;/span&gt;
&lt;span class="c"&gt;# libgcc_s.so.1&lt;/span&gt;
&lt;span class="c"&gt;# libc.so.6&lt;/span&gt;
&lt;span class="c"&gt;# (nada de smtp libs ni openssl)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Paridad bit-a-bit &lt;code&gt;fitz run&lt;/code&gt; ↔ &lt;code&gt;fitz build&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;El intérprete (&lt;code&gt;fitz run&lt;/code&gt;) y el codegen a Rust (&lt;code&gt;fitz build&lt;/code&gt;) emiten el mismo wire SMTP. Mismo handshake, mismo SCRAM auth, mismo Message-ID. Bug en un path = bug en el otro. Tests E2E lo validan en cada commit.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Async desde día uno
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;smtp.send(...)&lt;/code&gt; devuelve &lt;code&gt;Future&amp;lt;Result&amp;lt;SmtpResult&amp;gt;&amp;gt;&lt;/code&gt;. Integra natural con el resto del stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@background
async fn send_welcome(email: Str) -&amp;gt; Null {
    let _ = smtp.send({
        "to": email,
        "subject": "Bienvenido",
        "body": "Hola, gracias por unirte.",
    }).await
    return null
}

@post("/signup")
fn signup(input: SignupInput) {
    // ... crear el user en la DB ...
    let _ = spawn(send_welcome(input.email))
    return 201 { "id": new_user_id }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El handler responde 201 al cliente &lt;strong&gt;inmediato&lt;/strong&gt;. El email se manda en otro task tokio. Si el SMTP server tarda 2 segundos, no le importa al cliente — ya respondió hace 2 segundos.&lt;/p&gt;

&lt;p&gt;Y con &lt;code&gt;@cron&lt;/code&gt; armás digests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@cron("0 0 9 * * *")  // todos los días 09:00
async fn daily_digest() -&amp;gt; Null {
    let r = smtp.send({
        "to": "team@example.com",
        "subject": "Daily digest",
        "body_html": "&amp;lt;p&amp;gt;Hoy: ...&amp;lt;/p&amp;gt;",
    }).await
    match r {
        Ok(_) =&amp;gt; log.info("digest.sent"),
        Err(e) =&amp;gt; log.error("digest.failed", error: e),
    }
    return null
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; como modelo de error
&lt;/h3&gt;

&lt;p&gt;Errores de transporte (DNS, auth, TLS, server reject) llegan como &lt;code&gt;Result::Err(Str)&lt;/code&gt; con prefijo &lt;code&gt;"smtp: "&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;match smtp.send(opts).await {
    Ok(r) =&amp;gt; log.info("smtp.delivered", message_id: r.message_id),
    Err(e) =&amp;gt; {
        // El prefijo te permite clasificar:
        // "smtp: server rejected mail: ..."     → 5xx del server
        // "smtp: transient error: ..."          → 4xx temporario
        // "smtp: client error: ..."             → TLS/auth fail
        // "smtp: invalid `to` address ..."      → parse error
        log.warn("smtp.failed", error: e)
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El checker estático exige manejo. Si tu fn retorna &lt;code&gt;Result&amp;lt;...&amp;gt;&lt;/code&gt;, podés propagar con &lt;code&gt;?&lt;/code&gt;. Si no, te exige &lt;code&gt;match&lt;/code&gt;. Cero excepciones runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Magic-link auth en 30 líneas
&lt;/h3&gt;

&lt;p&gt;El caso showcase del stack web first-class de Fitz: HTTP server-side + auth con &lt;code&gt;jwt.encode&lt;/code&gt; + SMTP outbound, todo combinado en un binario.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;type EmailRequest {
    email: Str
}

@post("/auth/magic-link")
async fn magic_link(input: EmailRequest) -&amp;gt; Result&amp;lt;Str&amp;gt; {
    let payload = { "email": input.email }
    let token = jwt.encode(payload, "secret")
    let link = "https://app.example.com/verify?t={token}"
    let r = smtp.send({
        "to": input.email,
        "subject": "Tu link de login",
        "body_text": "Hacé click acá:\n{link}\n\nExpira en 5 min.",
        "body_html": "&amp;lt;p&amp;gt;Hacé click &amp;lt;a href=\"{link}\"&amp;gt;acá&amp;lt;/a&amp;gt;.&amp;lt;/p&amp;gt;",
    }).await?
    return Ok(r.message_id)
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sin Auth0, sin Supabase, sin Stripe webhooks. Un binario, un puerto, un comando para deployarlo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup local con MailHog
&lt;/h2&gt;

&lt;p&gt;Para dev no querés mandar mails reales. &lt;a href="https://github.com/mailhog/MailHog" rel="noopener noreferrer"&gt;MailHog&lt;/a&gt; es un SMTP server fake con UI web que retiene los mails sin reenviar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; mailhog &lt;span class="nt"&gt;-p&lt;/span&gt; 1025:1025 &lt;span class="nt"&gt;-p&lt;/span&gt; 8025:8025 mailhog/mailhog
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SMTP_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;localhost
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SMTP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1025
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;SMTP_TLS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;none
fitz run app.fitz
&lt;span class="c"&gt;# Después abrís http://localhost:8025 en el browser.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En producción cambiás las env vars a Gmail/SES/SendGrid/Mailgun/lo que sea — el código de Fitz no cambia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs honestos (MVP)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sin attachments todavía&lt;/strong&gt;. La key &lt;code&gt;attachments&lt;/code&gt; se rechaza con error claro. Workaround: bundle attachments via S3/object storage + link en el body. Lo más limpio para producción serio igual.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin &lt;code&gt;smtp.configure(...)&lt;/code&gt; programático&lt;/strong&gt;. Config sale solo de env vars hoy. Si necesitás multi-tenant con un SMTP por tenant, te queda como deuda post-MVP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin retry built-in&lt;/strong&gt;. Si &lt;code&gt;smtp.send&lt;/code&gt; falla, el caller decide qué hacer. El patrón canónico para producción es tabla de outbox + &lt;code&gt;@cron&lt;/code&gt; con retry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Esto NO es FUD — son decisiones explícitas del MVP. Los 3 son refinables si entra demanda real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cierre
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;smtp.send&lt;/code&gt; no es "una librería más fácil". Es &lt;strong&gt;el mismo modelo mental&lt;/strong&gt; que ya usás en el resto de Fitz: &lt;code&gt;Result&amp;lt;T&amp;gt;&lt;/code&gt; como retorno, &lt;code&gt;?&lt;/code&gt; para propagar, &lt;code&gt;.await&lt;/code&gt; para async, &lt;code&gt;@background + spawn&lt;/code&gt; para fire-and-forget.&lt;/p&gt;

&lt;p&gt;Si ya sabés cómo encadenar &lt;code&gt;http.get(...).await?&lt;/code&gt; o &lt;code&gt;db.query(...).await?&lt;/code&gt;, ya sabés cómo mandar mails. Cero API nuevo que aprender.&lt;/p&gt;

&lt;p&gt;Y cuando fitzwatch reanude, la línea que tenía pendiente para notificar incidents pasa de "webhook a n8n + traducción a SMTP" a &lt;code&gt;smtp.send({...}).await?&lt;/code&gt;. Una línea. Eso es Fitz.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Probalo&lt;/strong&gt;: &lt;code&gt;fitz&lt;/code&gt; versión &lt;code&gt;0.18.0+&lt;/code&gt; (ya en &lt;a href="https://github.com/Thegreekman76/fitz/releases/latest" rel="noopener noreferrer"&gt;GitHub releases&lt;/a&gt;). Cap exhaustivo + 3 ejemplos runnable contra MailHog en la &lt;a href="https://thegreekman76.github.io/fitz/guide/#17-http-nativo" rel="noopener noreferrer"&gt;sub-sección "SMTP outbound" del cap 17 de la guía&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mantenete al día&lt;/strong&gt;: la serie Fitz se publica en &lt;a href="https://dev.to/"&gt;dev.to&lt;/a&gt; cada vez que cerramos una mini-tanda grande. La próxima cubrirá una feature concreta detectada durante fitzwatch.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>rust</category>
      <category>email</category>
    </item>
  </channel>
</rss>
