<rss version="2.0">
  <channel>
    <title>Programming on Nathan Heller</title>
    <link>https://blog.naheller.com/categories/programming/</link>
    <description></description>
    
    <language>en</language>
    
    <lastBuildDate>Mon, 25 May 2026 16:00:34 +0700</lastBuildDate>
    
    <item>
      <title>Tracking books with webhooks</title>
      <link>https://blog.naheller.com/2026/05/25/tracking-books-with-webhooks/</link>
      <pubDate>Mon, 25 May 2026 16:00:34 +0700</pubDate>
      
      <guid>http://nxte.micro.blog/2026/05/25/tracking-books-with-webhooks/</guid>
      <description>&lt;p&gt;One of my favorite things about small projects is the way in which their constraints can inspire unexpected solutions. You might expect a simple design to limit the room for creativity, but in my experience the opposite is often true.&lt;/p&gt;
&lt;p&gt;With this project I had the good fortune to stumble into a solution that 1) taught me something new, and 2) leveraged each puzzle piece&amp;rsquo;s natural features in a way that preserved the minimalism I was going for.&lt;/p&gt;
&lt;p&gt;In this case the puzzle pieces were Netlify (with its build hooks) and Google Forms (with its Apps Script platform integration).&lt;/p&gt;
&lt;h2 id=&#34;a-book-tracker&#34;&gt;A book tracker&lt;/h2&gt;
&lt;p&gt;The idea was to build a quick and easy way for my wife and I to log the books we finish in a shared list. We didn&amp;rsquo;t need a full social media platform like Goodreads, and in fact the UI cruft there added friction to the book entry process. Likewise our phones&amp;rsquo; Notes app was too simplistic, missing basic features like sorting and filtering. But something relatively low-tech seemed best, given our limited requirements and reluctance to buy into another app or ecosystem.&lt;/p&gt;
&lt;p&gt;So we landed on a spreadsheet, which appealed to me as a data store, but fell short as a view/edit layer, since we mostly planned to be using the tool on our phones. Pinch-zooming around a spreadsheet on a 6-inch screen isn&amp;rsquo;t much fun. Google Forms tied the data entry side together as it had decent mobile UX and could be set up to feed responses into a spreadsheet. Only the view layer remained.&lt;/p&gt;
&lt;p&gt;Lately I&amp;rsquo;ve been reaching for vanilla JavaScript and HTML more often when building simple websites. After all, isn&amp;rsquo;t React just the Goodreads of frontend frameworks? That&amp;rsquo;s not the low-tech spirit we&amp;rsquo;re looking for here. We need only fetch our books from the Google Sheets API and display them in a single column list. Add some basic sort/filter options and call it a day. That has Web 1.0 written all over it.&lt;/p&gt;
&lt;p&gt;In order to hide my API key from prying eyes, I decided to make the call server-side, which is like, even more Web 1.0 since the earliest browsers didn&amp;rsquo;t even run JavaScript.&lt;/p&gt;
&lt;p&gt;For hosting I reached for the friendly and familiar (and free) Netlify, which has served me well over the years. In effect I&amp;rsquo;d write my own barebones static site generator in a single JS file, which would insert the book data into an HTML string and write it to an &lt;code&gt;index.html&lt;/code&gt; file. Netlify would run that JS file at build time and serve the HTML.&lt;/p&gt;
&lt;h2 id=&#34;a-build-hook&#34;&gt;A build hook&lt;/h2&gt;
&lt;p&gt;For a while I thought I&amp;rsquo;d finished. I&amp;rsquo;m not proud to admit how long I scratched my head when newly added books didn&amp;rsquo;t appear on the site. Then I pushed a small code change to Github, which triggered a new Netlify build, and suddenly the books appeared. Of course. The Google Sheets API fetch only happened at build time. I needed an automated way to tell Netlify whenever a new book was added to the spreadsheet.&lt;/p&gt;
&lt;p&gt;Another admission: in my 8 years as a frontend engineer, I&amp;rsquo;ve never really taken the time to learn about webhooks. I&amp;rsquo;d only heard about them in passing and had a vague notion that they could connect disparate web services. Since educating myself on the concept, I&amp;rsquo;m glad to report that I wasn&amp;rsquo;t too far off the mark. Red Hat sums it up:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A webhook is a lightweight, event-driven communication that automatically sends data between applications via HTTP&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I poked around my Netlify dashboard hoping to find something useful. Thankfully they&amp;rsquo;ve thought of just about everything (this is not a sponsored post), and I eventually lit upon a build hooks section. It promised to give me a unique URL that I could use to trigger a build, and by golly, it did just that.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;curl -X POST -d &amp;#39;{}&amp;#39; https://api.netlify.com/build_hooks/&amp;lt;my-hook-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;With hook in hand, I headed back over to the Googleplex to figure out whether I could make it sing my particular tune.&lt;/p&gt;
&lt;h2 id=&#34;an-app-script&#34;&gt;An app script&lt;/h2&gt;
&lt;p&gt;The Google &lt;del&gt;minefield&lt;/del&gt; suite of products and services is a daunting place to find yourself. On the developer side, it feels like every other feature is buried behind layers of usage agreements, consent screens, and granular credentials that rival the &lt;del&gt;worst&lt;/del&gt; best AWS has to offer.&lt;/p&gt;
&lt;p&gt;Thankfully the Apps Script page is mercifully minimal and requires only a modest amount of documentation to comprehend. Once my reading had convinced me it was possible, I set about creating a trigger that would run whenever my Google Form was used to submit a new book. My little function amounted to no more than a fetch request:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;function&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;onFormSubmit&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;WEBHOOK_URL&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;https://api.netlify.com/build_hooks/&amp;lt;my-hook-id&amp;gt;&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;options&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;method&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;post&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;headers&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;Content-Type&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;application/json&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;payload&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;:&lt;/span&gt; {},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	};
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;UrlFetchApp&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;fetch&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;WEBHOOK_URL&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;options&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I then set up a new trigger that called this function on form submit. Interestingly, the other event type option was on form &lt;em&gt;open&lt;/em&gt;. Seems like jumping the gun there a bit, but to each their own.&lt;/p&gt;
&lt;p&gt;Once I&amp;rsquo;d wired this up I was finally able to trigger new website builds whenever a book was added. This wasn&amp;rsquo;t instantaneous, but a dozen seconds is perfectly adequate considering we rarely rush right over to check. I&amp;rsquo;ll admit I did rush over a few times when I first got it working. It&amp;rsquo;s cool, okay? It works! And not a dime spent&lt;/p&gt;
</description>
    </item>
    
  </channel>
</rss>