<?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>Forem: Taylor Beseda</title>
    <description>The latest articles on Forem by Taylor Beseda (@tbeseda).</description>
    <link>https://forem.com/tbeseda</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F160716%2Fb256d88a-de08-4ee3-8a2c-a7eb8adce057.jpg</url>
      <title>Forem: Taylor Beseda</title>
      <link>https://forem.com/tbeseda</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tbeseda"/>
    <language>en</language>
    <item>
      <title>Air Quality App with Enhance</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Wed, 12 Jul 2023 17:30:31 +0000</pubDate>
      <link>https://forem.com/begin/air-quality-app-with-enhance-1c8d</link>
      <guid>https://forem.com/begin/air-quality-app-with-enhance-1c8d</guid>
      <description>&lt;p&gt;With wildfire season well upon us in North America, it’s a good idea to keep an eye on local air quality.&lt;br&gt;
Let’s get some real time data from the US EPA’s &lt;a href="https://www.airnow.gov/"&gt;AirNow&lt;/a&gt; program.&lt;br&gt;
Even with a limited API request budget, we can get snappy results by caching and refreshing data on demand.&lt;br&gt;
All with features already built into &lt;a href="https://enhance.dev"&gt;Enhance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://invent-k6b.begin.app/"&gt;Try the AQI app now.&lt;/a&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  AirNow API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://www.airnowapi.org/aq/observation/zipCode/current/?format=application/json&amp;amp;API_KEY=_SECRET_&amp;amp;zipCode=90210
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s it, that’s the URL we’ll use to request the air quality index (AQI) for any given US zip code*.&lt;br&gt;
We get back an array of one to three measurements if a weather station is found near the requested zip code.&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="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;AQI&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;58&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Moderate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;DateObserved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2023-06-08 &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;HourObserved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;LocalTimeZone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ParameterName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;O3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ReportingArea&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Denver-Boulder&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;StateCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CO&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;We’ll need to keep in mind that &lt;a href="https://docs.airnowapi.org/faq#rateLimits"&gt;this API limits us to 500 requests per hour&lt;/a&gt;, however the docs also let us know that most data points are updated once each hour.&lt;br&gt;
So if our application caches its copy of that data for 15 min, we can query 125 unique zip codes each hour - probably a good start, at least until it goes viral 😉&lt;/p&gt;

&lt;p&gt;*For simplicity, I’m sticking with US data, but international data is available.&lt;br&gt;
I recommend checking out&lt;a href="https://api-docs.iqair.com/"&gt; IQAir&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Databases in Enhance Apps
&lt;/h2&gt;

&lt;p&gt;To cache AQI info we’ll need to stash data in a database.&lt;br&gt;
And it needs to be fast!&lt;/p&gt;

&lt;p&gt;Enhance is built on top of &lt;a href="https://arc.codes"&gt;Architect&lt;/a&gt; and comes with all of Arc’s superpowers for free.&lt;br&gt;
These features (database access, scheduled functions, event queues, and more) are all opt-in and don’t bloat your deployed project.&lt;/p&gt;

&lt;p&gt;The data layer is powered by DynamoDB from AWS, so it’s incredibly quick to set and get data for a page request.&lt;br&gt;
Like, &amp;lt;10ms fast.&lt;/p&gt;

&lt;p&gt;The simplest way to start with database operations is to install &lt;a href="https://www.npmjs.com/package/@begin/data"&gt;@begin/data&lt;/a&gt; (this library can be used with any Arc/Enhance app, even if you deploy to your own AWS account).&lt;/p&gt;

&lt;p&gt;Here’s a sample of usage:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@begin/data&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;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pizza&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt; &lt;span class="c1"&gt;// save multiple pizzas at once&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bbq-chkn&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chicken&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="s1"&gt;chz&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="s1"&gt;bbq sauce&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;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;southwest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chilis&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="s1"&gt;red onion&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="s1"&gt;corn&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="s1"&gt;chz&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;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hawaiian&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;chz&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="s1"&gt;ham&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="s1"&gt;pineapple&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;const&lt;/span&gt; &lt;span class="nx"&gt;bestPizza&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hawaiian&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bestPizza&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toppings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&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="c1"&gt;// 'pineapple'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cache API Requests
&lt;/h2&gt;

&lt;p&gt;I’ve created a simple form component that GETs the /us route with a zip query parameter.&lt;br&gt;
My Enhance API function will then lookup the most recent AQI data for that zip code.&lt;br&gt;
But a visitor could refresh that page several times over the course of an hour, burning through my allotted AirNow API limit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--dQs29WKh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h9k1y52xuttvjoqubbw8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--dQs29WKh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/h9k1y52xuttvjoqubbw8.png" alt="AQI app form with single zip code field" width="376" height="124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So when the data is fetched the first time, I’ll cache the response with a time-to-live (TTL) value of 15 min - AirNow’s data rarely changes for a given location more than once in an hour.&lt;br&gt;
Then, when that user refreshes or someone else requests the same zip code, I’ll check the database first and only query the API if a record doesn’t exist for that zip.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@begin/data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;AIRNOW_URL&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aqi&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;zip&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="c1"&gt;// just render the form&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cacheKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`zip:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;aqiData&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// first, check the cache&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cacheKey&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;cached&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;aqiData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;aqiData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aqiData&lt;/span&gt; &lt;span class="c1"&gt;// continue with data we have&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// no cache, go get some data from AirNow&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;AIRNOW_URL&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;`&amp;amp;zip=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="nx"&gt;aqiData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;set&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="c1"&gt;// cache the new data&lt;/span&gt;
        &lt;span class="nx"&gt;table&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;cacheKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nx"&gt;aqiData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;ttl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="c1"&gt;// ↑ 15 minutes from now as seconds since epoch&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;aqiData&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;Granted this doesn’t have much error handling, but it serves as a great example of how simple my API layer is here.&lt;/p&gt;

&lt;p&gt;Now we can present this data in our views powered by server-rendered custom elements and browser-native web components! I won’t get into the specifics of creating SVG meters (inspired by &lt;a href="https://breathable.app"&gt;breathable.app&lt;/a&gt;), but here’s what I worked up:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eGyIQ-pw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kxktcoahnoim0dq0s8vd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eGyIQ-pw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kxktcoahnoim0dq0s8vd.png" alt="Screenshot of the full AQI app interface" width="772" height="1065"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Extended Example
&lt;/h2&gt;

&lt;p&gt;In my full example I also grab a visitor’s likely zip code based on their IP address and serve that result from the index route.&lt;br&gt;
Not only is the AQI data cached, but so is the zip code lookup since that service also has daily limits.&lt;/p&gt;

&lt;p&gt;Interactive example here: &lt;a href="https://invent-k6b.begin.app/"&gt;https://invent-k6b.begin.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the source code: &lt;a href="https://github.com/enhance-dev/enhance-example-aqi"&gt;https://github.com/enhance-dev/enhance-example-aqi&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can even see the range of meter values: &lt;a href="https://invent-k6b.begin.app/test"&gt;https://invent-k6b.begin.app/test&lt;/a&gt;&lt;/p&gt;

</description>
      <category>enhance</category>
      <category>database</category>
      <category>aws</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Tested: Database Providers on Lambda</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Tue, 06 Jun 2023 23:27:09 +0000</pubDate>
      <link>https://forem.com/begin/tested-database-providers-on-lambda-2e59</link>
      <guid>https://forem.com/begin/tested-database-providers-on-lambda-2e59</guid>
      <description>&lt;p&gt;The last couple years have seen the rise of third party database providers, Database as a Service (DBaaS).&lt;br&gt;
Instead of hosting your database on the same box as your primary application server, developers can use an external db host.&lt;br&gt;
Often called "serverless" databases, these offerings offload the responsibility of maintaining a database appliance.&lt;br&gt;&lt;br&gt;
Providers also often offer useful dashboards, data browsing, branching, schema versioning, and more.&lt;/p&gt;
&lt;h2&gt;
  
  
  Selecting a Database
&lt;/h2&gt;

&lt;p&gt;When adding a data layer to any application, choosing a database type, engine, and, now, provider is an important choice.&lt;/p&gt;
&lt;h3&gt;
  
  
  Database Paradigm + Speed
&lt;/h3&gt;

&lt;p&gt;Probably the most important factor when choosing a database is deciding on "relational" tables like SQL or "document" storage, often called NoSQL.&lt;br&gt;
But for this experiment, we'll set that aside and focus on the second most important consideration: access speed.&lt;/p&gt;

&lt;p&gt;Specifically, we'll look at how fast the simplest queries are from a Lambda (deployed to AWS with the vanilla Node.js runtime) to various third party db vendors.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Tests
&lt;/h2&gt;

&lt;p&gt;I've created an &lt;a href="https://arc.codes"&gt;Architect&lt;/a&gt; application (hosted on &lt;a href="https://begin.com"&gt;Begin&lt;/a&gt;) that's made up of several functions: one for testing each provider and one to provide a web view of embedded &lt;code&gt;&amp;lt;iframes&amp;gt;&lt;/code&gt; with the results of each.&lt;/p&gt;

&lt;p&gt;Each test implementation performs essentially the same query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code and more technical explanation is available on &lt;a href="https://awaken-un3.begin.app/"&gt;the actual test page&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YDh8BorR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjkj5ursyv8gf7n3e5r3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YDh8BorR--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tjkj5ursyv8gf7n3e5r3.png" alt="image of sample data from live test" width="800" height="876"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Sampled Speeds
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Driver&lt;/th&gt;
&lt;th&gt;Approx. Query Time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://neon.tech"&gt;Neon&lt;/a&gt; &lt;sup&gt;1&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;300ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@neondatabase/serverless&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;a href="https://supabase.com"&gt;Supabase&lt;/a&gt; &lt;sup&gt;2&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;450ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;REST API via &lt;code&gt;fetch&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
25 - 375ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://planetscale.com"&gt;PlanetScale&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mysql2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;125ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@planetscale/database&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
25 - 150ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://mongodb.com"&gt;MongoDB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mongodb&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;725ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://aws.amazon.com/dynamodb/"&gt;DynamoDB&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;@architect/functions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;sup&gt;1&lt;/sup&gt; This does not include the cold start.&lt;br&gt;
The "wake" time can exceed 5s (5,000ms), but once active is 0.&lt;br&gt;
Neon is in early access and is working on various (paid) ways to manage this penalty.&lt;/p&gt;

&lt;p&gt;&lt;sup&gt;2&lt;/sup&gt; Supabase's &lt;code&gt;@supabase/supabase-js&lt;/code&gt; was not tested as it requires a build step on install (my CD environment, Lambda, doesn't have node-gyp).&lt;br&gt;
I expect it would perform similarly to their REST API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regional Differences
&lt;/h3&gt;

&lt;p&gt;The above sampling is for tests where the database provider is &lt;em&gt;always&lt;/em&gt; in a different US region from the Lambdas that connect to them.&lt;br&gt;&lt;br&gt;
The only provider sharing a region with the Lambda is Dynamo since both resources will naturally be created in the same AWS region.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;All providers get a significant speed boost&lt;/strong&gt; when in the same region as your Lambda and using the "native" driver.&lt;/p&gt;



&lt;p&gt;For example, when both Lambda and Supabase are in &lt;code&gt;us-east-1&lt;/code&gt;, the same query with &lt;code&gt;postgres&lt;/code&gt; takes ~50ms: 9x faster 🔥&lt;/p&gt;



&lt;p&gt;(Limited tests were conducted in shared regions but are not demonstrated live.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Considerations
&lt;/h3&gt;

&lt;p&gt;These tests do not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;attempt to pool or keep-alive connections&lt;/li&gt;
&lt;li&gt;snapshot results or track variance over time&lt;/li&gt;
&lt;li&gt;test subsequent queries&lt;/li&gt;
&lt;li&gt;use a large dataset or a variety of DB operations&lt;/li&gt;
&lt;li&gt;thoroughly consider resource regions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;p&gt;Not surprisingly, AWS's own DynamoDB is the fastest way to query data from a Lambda-based application.&lt;br&gt;
Its repeatable 10ms query latency is 2.5 times better than the closest competitor's best results.&lt;br&gt;
We acknowledge that it may be intimidating to get started with a NoSQL database, and that's why we provide &lt;a href="https://www.npmjs.com/package/@begin/data"&gt;&lt;code&gt;@begin/data&lt;/code&gt;&lt;/a&gt; as an abstraction layer on top of DynamoDB.&lt;br&gt;
For folks who want to learn more about DynamoDB we recommend &lt;a href="https://www.dynamodbbook.com/"&gt;Alex DeBrie's The DynamoDB Book&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That said, all tested provider queries are less than half a second (except MongoDB - however, their paid tiers do reach that 500ms threshold)!&lt;/p&gt;

&lt;p&gt;Ultimately any database is better than no database.&lt;br&gt;
Don't be paralyzed or resort to throwing the kitchen sink at the problem. Pick one and get to building the initial implementation.  &lt;/p&gt;

</description>
      <category>database</category>
      <category>lambda</category>
      <category>performance</category>
      <category>node</category>
    </item>
    <item>
      <title>Begin Domains beta</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Tue, 04 Apr 2023 17:39:32 +0000</pubDate>
      <link>https://forem.com/begin/begin-domains-beta-5an2</link>
      <guid>https://forem.com/begin/begin-domains-beta-5an2</guid>
      <description>&lt;p&gt;Today we're excited to announce the beta release of Begin Domains. This early access feature allows you to subscribe to a domain name and link it to a Begin application. You can subscribe to a domain name using the new &lt;code&gt;begin domains&lt;/code&gt; command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Deploy an application
&lt;/h3&gt;

&lt;p&gt;First, you'll need to have an application deployed to Begin; this is free. To learn about deploying applications with Begin, check out the &lt;a href="https://begin.com/docs/"&gt;Getting Started&lt;/a&gt; guide.&lt;/p&gt;

&lt;h3&gt;
  
  
  Subscribe to a domain
&lt;/h3&gt;

&lt;p&gt;Once you have an application deployed, you can subscribe to a domain name. Use the &lt;code&gt;begin domains add&lt;/code&gt; command to both check the availability of a domain and get a checkout link for the domain.&lt;br&gt;
For this example, I'll use &lt;code&gt;your-name.codes&lt;/code&gt; as the domain name.&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="o"&gt;&amp;gt;&lt;/span&gt; begin domains add &lt;span class="nt"&gt;--domain&lt;/span&gt; your-name.codes
your-name.codes is available!
Subscribe here: https://api.begin.com/checkout/XHB1WDPM
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow the checkout link to subscribe to the domain name. If the domain is not available, some suggestions will be provided.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⏳ Wait for DNS propagation
&lt;/h3&gt;

&lt;p&gt;Once you've subscribed to a domain name, you'll need to wait for DNS propagation. Unfortunately, we can't do much to speed this along, but you can check the status of your domain with the &lt;code&gt;begin domains list&lt;/code&gt; command. Add the &lt;code&gt;--verbose&lt;/code&gt; flag to see additional details.&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="o"&gt;&amp;gt;&lt;/span&gt; begin domains list &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Link a domain to an environment
&lt;/h3&gt;

&lt;p&gt;Now the fun part: link the new domain to an existing application environment. Use the &lt;code&gt;begin domains link&lt;/code&gt; command to link a domain to an environment.&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="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-app
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; begin domains &lt;span class="nb"&gt;link&lt;/span&gt; &lt;span class="nt"&gt;--domain&lt;/span&gt; your-name.codes &lt;span class="nt"&gt;--env&lt;/span&gt; production
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll provision the resources (SSL cert, DNS, etc.) for your app, so hang tight. Soon, you'll see your domain is linked!&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="o"&gt;&amp;gt;&lt;/span&gt; begin domains list
your-name.codes
└─→ my-app → &lt;span class="s2"&gt;"production"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Learn More
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://begin.com/docs/cli/commands/domain-subscriptions"&gt;Find additional info on the new Domains feature in our docs.&lt;/a&gt; Including a roadmap of upcoming features.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://begin.com/docs/cli/commands/domains"&gt;Read up on the &lt;code&gt;domains&lt;/code&gt; command reference.&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Combine Utility CSS with Scoped Styles in an Enhance Project</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Tue, 20 Dec 2022 17:15:54 +0000</pubDate>
      <link>https://forem.com/tbeseda/combine-utility-css-with-scoped-styles-in-an-enhance-project-50ch</link>
      <guid>https://forem.com/tbeseda/combine-utility-css-with-scoped-styles-in-an-enhance-project-50ch</guid>
      <description>&lt;p&gt;[A post from my drafts folder]&lt;/p&gt;

&lt;p&gt;Enhance projects include a (totally optional) built-in CSS utility class system out of the box. By default, the generated stylesheet is very small, and developers can even configure their app’s design scale and color palette.&lt;/p&gt;

&lt;p&gt;Let’s create a form layout with the available layout helpers and stylize the submit button with custom colors. For this approach, we’ll blend utility classes and a custom element’s local stylesheet.&lt;/p&gt;

&lt;p&gt;Starting with a barebones pizza order form:&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;main&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Order a Pizza!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;fieldset&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;legend&amp;gt;&lt;/span&gt;Pie Size&lt;span class="nt"&gt;&amp;lt;/legend&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"size_1"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"small"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"size_1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Small&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"size_2"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"medium"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"size_2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Medium&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"size_3"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"large"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"size_3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Large&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/fieldset&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;fieldset&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;legend&amp;gt;&lt;/span&gt;Toppings!&lt;span class="nt"&gt;&amp;lt;/legend&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Choose one or more &lt;span class="nt"&gt;&amp;lt;small&amp;gt;&lt;/span&gt;(cmd + click)&lt;span class="nt"&gt;&amp;lt;/small&amp;gt;&lt;/span&gt;:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"toppings"&lt;/span&gt; &lt;span class="na"&gt;multiple&lt;/span&gt; &lt;span class="na"&gt;size=&lt;/span&gt;&lt;span class="s"&gt;"12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;optgroup&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Premium toppings"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"pep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Pepperoni&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"jal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Jalapenos&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"oliv"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Green olives&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/optgroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;optgroup&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Regular toppings"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"pine"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Pineapple&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"feta"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Feta cheese&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"bell"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Bell pepper&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/optgroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;optgroup&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Free toppings"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"saus"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Italian Sausage&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"ham"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Ham&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"spin"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Spinach&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/optgroup&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/fieldset&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;Place My Order&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;/main&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Well, that doesn’t look like much:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxvvk19nr8moj0u3whd91.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxvvk19nr8moj0u3whd91.png" alt="Image description" width="435" height="385"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order to give us a clean slate, Enhance Styles’ CSS reset is fairly aggressive. No problem, let’s start by building up a better layout.&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;style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50ch&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;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt4 m-auto font-sans"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text2 text-center mb1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Order a Pizza!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right away, I’ve added a &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag that scoped just to this page. I’ve set a max-width and then added our first utility classes to the &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; tag to get a bit of margin and use a sans-serif font stack. Similarly, I’ve set a text size and centering on the main &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;Let’s continue by adjusting each &lt;code&gt;&amp;lt;fieldset&amp;gt;&lt;/code&gt;’s padding and a border:&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;fieldset&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"p1 border2"&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;All elements have a border-color of transparent by default, so the next step is to set the fieldset’s border-color in our &lt;code&gt;&amp;lt;style&amp;gt;&lt;/code&gt; tag to a provided color swatch from Enhance Styles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;fieldset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--secondary-200&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;Taking a look now, and we’re in much better shape.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgujbs6dlyl8w66hkvcd8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgujbs6dlyl8w66hkvcd8.png" alt="Image description" width="680" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alright, time for some grid layout! First up, the form:&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;action=&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid flow-row gap-2"&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;And with our web inspector’s help, we can see the grid is coming together:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4l5lx29wxcd3eqim91bl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4l5lx29wxcd3eqim91bl.png" alt="Image description" width="703" height="529"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ll use the flex class to do something similar with the “Pie Size” &lt;code&gt;&amp;lt;fieldset&amp;gt;&lt;/code&gt;, notice the new &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; input/label wrappers:&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;fieldset&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-around p1 border2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;legend&amp;gt;&lt;/span&gt;Pie Size&lt;span class="nt"&gt;&amp;lt;/legend&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;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"size_1"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"small"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"size_1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Small&lt;span class="nt"&gt;&amp;lt;/label&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;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"size_2"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"medium"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"size_2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Medium&lt;span class="nt"&gt;&amp;lt;/label&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;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"size_3"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"large"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"size_3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Large&lt;span class="nt"&gt;&amp;lt;/label&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;/fieldset&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a similar treatment for the “Toppings!” section:&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;fieldset&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid flow-row gap-2 p1 border2"&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;We’re almost there&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkl2drmbqdjn1sbzfw5gu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkl2drmbqdjn1sbzfw5gu.png" alt="Image description" width="715" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now for that submit button, some added CSS in our style tag to set colors&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--light&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary-400&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a wrapper div + some utility classes&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;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt-2 flex justify-around"&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="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pt-4 pb-4 pl-1 pr-1 font-bold text1 radius1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Place My Order
  &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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we’ve got a nice-to-use Pizza Order Form&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fugtrbel3h8n63t3dvsut.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fugtrbel3h8n63t3dvsut.png" alt="Image description" width="800" height="605"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The final markup, all in one Enhance page file:&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;style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;main&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50ch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;fieldset&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--secondary-200&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;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--light&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nl"&gt;background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--primary-400&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;span class="nt"&gt;&amp;lt;main&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"mt4 m-auto font-sans"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"text2 text-center mb1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Order a Pizza!&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt; &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid flow-row gap-2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;fieldset&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"flex justify-around p1 border2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;legend&amp;gt;&lt;/span&gt;Pie Size&lt;span class="nt"&gt;&amp;lt;/legend&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;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"size_1"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"small"&lt;/span&gt; &lt;span class="na"&gt;required&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"size_1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Small&lt;span class="nt"&gt;&amp;lt;/label&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;div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"size_2"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"medium"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"size_2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Medium&lt;span class="nt"&gt;&amp;lt;/label&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;div&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"radio"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"size"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"size_3"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"large"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"size_3"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Large&lt;span class="nt"&gt;&amp;lt;/label&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;/fieldset&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;fieldset&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"grid flow-row gap-2 p1 border2"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;legend&amp;gt;&lt;/span&gt;Toppings!&lt;span class="nt"&gt;&amp;lt;/legend&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;label&amp;gt;&lt;/span&gt;Choose one or more &lt;span class="nt"&gt;&amp;lt;small&amp;gt;&lt;/span&gt;(cmd + click)&lt;span class="nt"&gt;&amp;lt;/small&amp;gt;&lt;/span&gt;:&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;

      &lt;span class="nt"&gt;&amp;lt;select&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"toppings"&lt;/span&gt; &lt;span class="na"&gt;multiple&lt;/span&gt; &lt;span class="na"&gt;size=&lt;/span&gt;&lt;span class="s"&gt;"12"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;optgroup&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Premium toppings"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"pep"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Pepperoni&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"jal"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Jalapenos&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"oliv"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Green olives&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/optgroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;optgroup&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Regular toppings"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"pine"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Pineapple&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"feta"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Feta cheese&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"bell"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Bell pepper&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/optgroup&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;optgroup&lt;/span&gt; &lt;span class="na"&gt;label=&lt;/span&gt;&lt;span class="s"&gt;"Free toppings"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"saus"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Italian Sausage&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"ham"&lt;/span&gt; &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Ham&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;option&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"spin"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Spinach&lt;span class="nt"&gt;&amp;lt;/option&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/optgroup&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/select&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/fieldset&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;"mt-2 flex justify-around"&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="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pt-4 pb-4 pl-1 pr-1 font-bold text1 radius1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Place My Order
      &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;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>watercooler</category>
    </item>
    <item>
      <title>Selecting 3rd Party Web Components</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Mon, 12 Dec 2022 12:00:00 +0000</pubDate>
      <link>https://forem.com/begin/selecting-3rd-party-web-components-5hjk</link>
      <guid>https://forem.com/begin/selecting-3rd-party-web-components-5hjk</guid>
      <description>&lt;p&gt;&lt;small&gt;Cover image photo by &lt;a href="https://unsplash.com/@laam"&gt;Laam&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;As Web Components (WCs) continue to gain traction, it's becoming more common to see elements published as re-usable modules. This awesome trend helps new apps get off the ground and incorporate new features without having to implement complex functionality. Here are a few things to consider when selecting a third party WC for your project — &lt;a href="https://enhance.dev"&gt;Enhance&lt;/a&gt; or otherwise.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The dependency graph&lt;/li&gt;
&lt;li&gt;Progressive enhancement-ability&lt;/li&gt;
&lt;li&gt;Accessibility features&lt;/li&gt;
&lt;li&gt;Styling interface&lt;/li&gt;
&lt;li&gt;Component "entrypoint"&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Don't ship a framework for a single component
&lt;/h2&gt;

&lt;p&gt;Before &lt;a href="https://blog.begin.com/posts/2022-03-22-dont-npm-install-your-way-to-success"&gt;&lt;code&gt;npm install&lt;/code&gt;ing your way to success&lt;/a&gt;, check what that new WC includes.&lt;/p&gt;

&lt;p&gt;There are several lightweight libraries I might expect to see as dependencies (like Alpine.js or WC-specific utilities like &lt;a href="https://github.com/exalt/exalt"&gt;Exalt&lt;/a&gt; and &lt;a href="https://github.com/slimjs/slim.js"&gt;slimjs&lt;/a&gt;). A full front end framework, like React, for a component is wholly unnecessary.&lt;/p&gt;

&lt;p&gt;Of course, there are specialized front-end frameworks specifically for Web Components: Lit, Stencil, and FAST to name a few. These can be very beneficial, especially if your app is already making use of one. However, the overhead should be proportional to the surface area of the new component. So if you'd like to use a full design system with several elements and it requires all of FAST, go for it! But if it's just a fancy dark mode toggle that happens to be "built with XYZ", it may not be worth the cost of a framework.&lt;/p&gt;

&lt;p&gt;Additionally, many popular front-end frameworks support WCs by trying to export &lt;code&gt;toWebComponent&lt;/code&gt;, but still require framework overhead — even if it's bundled, it's big.&lt;/p&gt;

&lt;p&gt;Large, complex dependency graphs will only lead to upgrade woes and large server payloads for your users.&lt;/p&gt;

&lt;h2&gt;
  
  
  The component works without JS (or is the enhancement)
&lt;/h2&gt;

&lt;p&gt;There should be a way to either partially render the WC from the server or use it in a baseline state. The exception would be if the external component library is working to augment an existing feature that has its own baseline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; If the objective of the new component is to scroll a marquee of color-changing text, I wouldn't expect it to animate without JavaScript enabled. However, I would expect that my server can still render the text content so that it's readable by the user — even without that nostalgic text effect.&lt;/p&gt;

&lt;p&gt;I want my server to transmit (or static file to contain):&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;fancy-marquee&amp;gt;&lt;/span&gt;Web Components are RAD!&lt;span class="nt"&gt;&amp;lt;/fancy-marquee&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I should not have to provide the text content via a JS-only API or as an attribute that won't be immediately rendered by the browser. Especially for such a simple element.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessible out of the box
&lt;/h2&gt;

&lt;p&gt;Accessibility relates to the previous two points: don't transmit bloated, unused code to the browser and provide a baseline experience without JavaScript.&lt;/p&gt;

&lt;p&gt;If those requirements are met, then the WC should also be accessible by default. This means that the component should be keyboard navigable and have a focus state. It should also be able to be used with a screen reader. Check to see that the component is using semantic HTML and ARIA roles/attributes where appropriate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; As &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA"&gt;noted by MDN&lt;/a&gt;, if the component implements a progress bar, a native &lt;code&gt;&amp;lt;progress&amp;gt;&lt;/code&gt; element should be used.&lt;br&gt;&lt;br&gt;
However, if the component can't necessarily be represented as a standard HTML element, then ARIA attributes should be used to provide information to assistive technology:&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;fancy-marquee&lt;/span&gt;
  &lt;span class="na"&gt;role=&lt;/span&gt;&lt;span class="s"&gt;"marquee"&lt;/span&gt;
  &lt;span class="na"&gt;aria-live=&lt;/span&gt;&lt;span class="s"&gt;"off"&lt;/span&gt;
  &lt;span class="na"&gt;aria-label=&lt;/span&gt;&lt;span class="s"&gt;"Web Components are RAD!"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Web Components are RAD!
&lt;span class="nt"&gt;&amp;lt;/fancy-marquee&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The component is style-able (or "headless")
&lt;/h2&gt;

&lt;p&gt;If the added Web Component has an HTML interface, it should not be an exercise in browser dev tools gymnastics.&lt;/p&gt;

&lt;p&gt;In many cases it's helpful to have some default CSS applied to custom elements and their children. But if my application requires adjustments to text shadow colors and input border radius, I'm going to benefit from a well thought out styling API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Ideally, well-structured WCs will document CSS variables that can be set within the scope of the custom element:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;fancy-marquee&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--shadow-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Crimson&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;fancy-marquee&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--container-radius&lt;/span&gt;&lt;span class="p"&gt;:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Another good approach is to provide a list of class names that can be augmented with custom definitions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nt"&gt;fancy-marquee&lt;/span&gt; &lt;span class="nc"&gt;.headline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;text-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="n"&gt;Crimson&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;fancy-marquee&lt;/span&gt; &lt;span class="nc"&gt;.container&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;border-radius&lt;/span&gt;&lt;span class="p"&gt;:&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, be wary of importing CSS into .js files…&lt;/p&gt;

&lt;h2&gt;
  
  
  Including the component is straightforward
&lt;/h2&gt;

&lt;p&gt;Your new WC should not require a new build pipeline or several import statements.&lt;/p&gt;

&lt;p&gt;Consider how your project includes external code, particularly how your web server sends code to the browser. Does the new component plug-and-play?&lt;/p&gt;

&lt;p&gt;In Enhance, any file in your &lt;code&gt;node_modules&lt;/code&gt; folder can be automatically bundled and server to the browser via the &lt;code&gt;@bundles&lt;/code&gt; config in the project's &lt;code&gt;.arc&lt;/code&gt;:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@bundles
fancy-marquee './node_modules/fancy-marquee/index.js'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any page can now reference this bundle:&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;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"module"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/_public/bundles/fancy-marquee.mjs"&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;Or from an Enhance element:&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="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;FancyMarquee&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/_public/bundles/fancy-marquee.mjs`
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Examples and Takeaways
&lt;/h2&gt;

&lt;p&gt;I don't want to call out any specific libraries as "unworthy" because I'm stoked anyone is authoring reusable components with a focus on web standards and native browser features! So here are a few I've used and would recommend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.webcomponents.org/element/@alenaksu/json-viewer"&gt;&lt;code&gt;json-viewer&lt;/code&gt;&lt;/a&gt; - A Web Component to visualize JSON data in a tree view&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.mux.com/guides/video/mux-player"&gt;&lt;code&gt;mux-player&lt;/code&gt;&lt;/a&gt; - A drop in component for adding Mux videos into your web application&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/github/github-elements"&gt;&lt;code&gt;github-elements&lt;/code&gt;&lt;/a&gt; - GitHub's Web Component collection&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://abdmmar.github.io/wc-toast/"&gt;&lt;code&gt;wc-toast&lt;/code&gt;&lt;/a&gt; - Notifications component for everyone&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not all third party libraries will score an A+ on all of these points. But if you're looking for a new component, consider these factors when evaluating your options and use discretion when adding new dependencies to your project.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webcomponents</category>
      <category>enhance</category>
    </item>
    <item>
      <title>HTML Custom Element as a Feature API</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Fri, 09 Dec 2022 05:49:09 +0000</pubDate>
      <link>https://forem.com/begin/html-custom-element-as-a-feature-api-3h0g</link>
      <guid>https://forem.com/begin/html-custom-element-as-a-feature-api-3h0g</guid>
      <description>&lt;p&gt;Say you've found a helpful front-end library you'd like to use for a feature in your web app, but you aren't thrilled with the interface.&lt;br&gt;
It requires an awkward data structure or tedious markup.&lt;br&gt;
Let's use an &lt;a href="https://enhance.dev"&gt;Enhance&lt;/a&gt; &lt;a href="https://enhance.dev/docs/learn/starter-project/elements"&gt;custom element definition&lt;/a&gt; to use as an adapter.&lt;/p&gt;
&lt;h2&gt;
  
  
  JavaScript-less Charts with &lt;a href="http://chartscss.org/"&gt;charts.css&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;For this guide, I'll make use of a slick library called “&lt;a href="http://chartscss.org/"&gt;charts.css&lt;/a&gt;” — it's a pure CSS way to turn tabular data into really nice charts.&lt;/p&gt;

&lt;p&gt;Let's start with the end in mind. Here's the result we'd like to target using just HTML + charts.css:&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/tbeseda/embed/dyKdaLm?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Be sure to open and scroll through the HTML code here.&lt;br&gt;
That's a lot of markup! And a lot of repetition.&lt;br&gt;
But thankfully, it's just a standard HTML &lt;code&gt;&amp;lt;table&amp;gt;&lt;/code&gt; with a few special classes and some CSS variable declarations mixed inline.&lt;/p&gt;
&lt;h2&gt;
  
  
  Data structure
&lt;/h2&gt;

&lt;p&gt;Our 2016 Olympics medal data isn't likely in an HTML-ready format, so let's assume it comes from a database or API as an array of objects.&lt;br&gt;
We can return that set from our &lt;a href="https://enhance.dev/docs/learn/starter-project/api"&gt;Enhance API route&lt;/a&gt;:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;medals&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="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USA&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;37&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;38&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="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GBR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&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="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CHN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;26&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;medals&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Implementation
&lt;/h2&gt;

&lt;p&gt;Again, we'll start with the goal in mind and author the desired code we want to write for the above chart (CodePen example):&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;my-chart&lt;/span&gt;
  &lt;span class="na"&gt;data-key=&lt;/span&gt;&lt;span class="s"&gt;"medals"&lt;/span&gt;
  &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"bar"&lt;/span&gt;
  &lt;span class="na"&gt;heading=&lt;/span&gt;&lt;span class="s"&gt;"2016 Summer Olympics Medal Table"&lt;/span&gt;
  &lt;span class="na"&gt;value-key=&lt;/span&gt;&lt;span class="s"&gt;"Country"&lt;/span&gt;
  &lt;span class="na"&gt;value-names=&lt;/span&gt;&lt;span class="s"&gt;"Gold,Silver,Bronze"&lt;/span&gt;
  &lt;span class="na"&gt;multiple&lt;/span&gt;
  &lt;span class="na"&gt;show-labels&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/my-chart&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Super simple interface and even follows charts.css conventions without all the markup.&lt;br&gt;
Here is the Enhance element definition where I've implemented the above interface:&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="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;MyChart&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;html&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&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="s1"&gt;bar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heading&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;valueKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;valueNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;value-names&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;multiple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiple&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;showLabels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-labels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&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;const&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;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chartData&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allClasses&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="s1"&gt;charts-css&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;multiple&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multiple&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;showLabels&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;show-labels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
    &amp;lt;table class="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;allClasses&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;
      &amp;lt;caption&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/caption&amp;gt;
      &amp;lt;thead&amp;gt;
        &amp;lt;tr&amp;gt;
          &amp;lt;th scope="col"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valueKey&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/th&amp;gt;
          &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valueNames&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
            &lt;span class="s2"&gt;`&amp;lt;th scope="col"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/th&amp;gt;`&lt;/span&gt;
          &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
        &amp;lt;/tr&amp;gt;
      &amp;lt;/thead&amp;gt;
      &amp;lt;tbody&amp;gt;
        &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`
          &amp;lt;tr&amp;gt;
            &amp;lt;th scope="row"&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &amp;lt;/th&amp;gt;
            &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="s2"&gt;`--start: calc(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/100);`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="s2"&gt;`--size: calc(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/100);`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`--color: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="p"&gt;]&lt;/span&gt;
              &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;td style="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/td&amp;gt;`&lt;/span&gt;
            &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
          &amp;lt;/tr&amp;gt;
        `&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
      &amp;lt;/tbody&amp;gt;
    &amp;lt;/table&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;At its core, it's a function to loop over the &lt;code&gt;medals&lt;/code&gt; data structure to generate &lt;code&gt;&amp;lt;tr&amp;gt;&lt;/code&gt; elements for charts.css.&lt;br&gt;
The applied classes are informed by the element attributes added to the &lt;code&gt;&amp;lt;my-chart&amp;gt;&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;Whenever we use the &lt;code&gt;&amp;lt;my-chart&amp;gt;&lt;/code&gt; element in our app, this definition will be used by Enhance to render our custom element.&lt;br&gt;
Bonus: this custom element ships &lt;strong&gt;zero&lt;/strong&gt; browser JavaScript!&lt;/p&gt;

&lt;p&gt;Not only have we vastly improved the experience of creating new charts in our application, but we've centralized this adapter.&lt;br&gt;
We can iterate on it over time when we want to add features or when the shape of the data inevitably changes.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>webcomponents</category>
      <category>enhance</category>
    </item>
    <item>
      <title>Tools for testing Functional Web Apps</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Mon, 20 Dec 2021 21:38:21 +0000</pubDate>
      <link>https://forem.com/begin/tools-for-testing-functional-web-apps-52n1</link>
      <guid>https://forem.com/begin/tools-for-testing-functional-web-apps-52n1</guid>
      <description>&lt;p&gt;&lt;small&gt;Photo by &lt;a href="https://unsplash.com/photos/3GZi6OpSDcY"&gt;Nicolas Thomas&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;If you're building critical cloud functions to return API results, handle evented business operations (like &lt;a href="https://blog.begin.com/posts/2021-11-17-shopify-webhooks"&gt;Shopify webhooks&lt;/a&gt;), or render web views, you'll want to incorporate some tests. It's essential to test their internals, inputs, and outputs in a predictable context. We want a utilitarian toolchain to ensure core services function as expected. Where each test can run in isolation, in an unmodified Node.js context. The test suite should run quickly and deterministically; helpful in local development and ideal in CI, where computing resources might be limited.&lt;/p&gt;

&lt;p&gt;Our tests should be proportionate to our functions in scope and size. Ideally, tests are fast and small, just like the services they're testing. (We're &lt;a href="https://blog.begin.com/posts/2021-12-01-fat-function-anti-pattern"&gt;not building fat functions&lt;/a&gt;, right?)&lt;/p&gt;

&lt;p&gt;For the sake of brevity, this discussion is limited to a Node.js runtime, but the principles are the same for other environments. Additionally, we won't worry about testing user interfaces or varying browser environments; those utilities are another post entirely.&lt;/p&gt;

&lt;p&gt;So what's a good approach? Which libraries should be candidates?&lt;/p&gt;

&lt;h2&gt;
  
  
  A comparison
&lt;/h2&gt;

&lt;p&gt;Several frameworks with performant runners help execute atomic tests, even concurrently. Some important considerations are library capabilities (like assertions), package size, maturity, and level of maintenance. Let's look at a collection of the most popular, up to date modules on npm today:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Concurrent&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Updated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/avajs/ava"&gt;Ava&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;281 kB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;3.15.0&lt;/td&gt;
&lt;td&gt;2021-11-01&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://jasmine.github.io/setup/nodejs.html"&gt;Jasmine&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;47 kB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;3.10.0&lt;/td&gt;
&lt;td&gt;2021-10-13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://hapi.dev/module/lab"&gt;@hapi/lab&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;160 kB&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;24.4.0&lt;/td&gt;
&lt;td&gt;2021-11-09&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://mochajs.org/"&gt;Mocha&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;3.8  &lt;em&gt;&lt;strong&gt;MB&lt;/strong&gt;&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;9.1.3&lt;/td&gt;
&lt;td&gt;2021-10-15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://node-tap.org/"&gt;Node Tap&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;28.3  &lt;em&gt;&lt;strong&gt;MB&lt;/strong&gt;&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;15.1.5&lt;/td&gt;
&lt;td&gt;2021-11-26&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/substack/tape"&gt;tape&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;248 kB&lt;/td&gt;
&lt;td&gt;No&lt;sup&gt;1&lt;/sup&gt;
&lt;/td&gt;
&lt;td&gt;5.3.2&lt;/td&gt;
&lt;td&gt;2021-11-16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/lukeed/uvu"&gt;uvu&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;46 kB&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;0.5.2&lt;/td&gt;
&lt;td&gt;2021-10-08&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ol&gt;
&lt;li&gt;achievable with tape-esque libraries like &lt;code&gt;mixed-tape&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A note about Jest
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;"But where’s Jest?"&lt;/em&gt; you ask. Don’t get me wrong, I understand the appeal of a framework with so many pleasantries. Jest’s feature-set is impressive and battle-tested. Unfortunately, tools like Jest, in order to accomplish so much, are opinionated. Jest uses implicit globals and its own context. It may not execute code the same way our servers will. This pattern can require all sorts of configuration bloat and transpilation, making debugging (especially in CI) tedious. In my view, Jest is not appropriate for what we're testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unpacked module size
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Emphasis on sizes &amp;gt; 1 MB in the above table is intentional.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Since we're running our tests in a cloud environment (in addition to locally), disk space matters.&lt;/p&gt;

&lt;p&gt;Unfortunately, the library that most appeals to me, Node Tap, is just too large. At 28 MB, &lt;code&gt;tap&lt;/code&gt; isn't very portable and will occupy a large part of allotted space in an environment like AWS Lambda. Hopefully, this limitation won't always be an issue, but it's an important factor for now.&lt;/p&gt;

&lt;h2&gt;
  
  
  A recommended testing “stack”
&lt;/h2&gt;

&lt;p&gt;I think any of the above options are viable, depending on your use case and preference. For example, if BDD is preferable, &lt;code&gt;jasmine&lt;/code&gt; has you covered. &lt;code&gt;ava&lt;/code&gt; has excellent TypeScript support. &lt;code&gt;uvu&lt;/code&gt; is super fast and works with ESM. And if you're looking for staying power, &lt;code&gt;mocha&lt;/code&gt; has been around for nearly a decade!&lt;/p&gt;

&lt;p&gt;For us at Begin and &lt;a href="https://arc.codes/"&gt;Architect&lt;/a&gt;, &lt;a href="https://github.com/substack/tape"&gt;tape&lt;/a&gt; has been in use for several years. &lt;code&gt;tape&lt;/code&gt; has a stable and straightforward API, routine maintenance updates, and outputs &lt;a href="https://testanything.org/"&gt;TAP&lt;/a&gt;, making it really versatile. While TAP is legible, it's not the most human-readable format. Fortunately, several TAP reporters can help display results for developers. Until recently, Begin's TAP reporter of choice was &lt;code&gt;tap-spec&lt;/code&gt;. Sadly &lt;code&gt;tap-spec&lt;/code&gt; wasn't kept up to date and npm began reporting vulnerabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  A new TAP reporter
&lt;/h3&gt;

&lt;p&gt;Enter &lt;a href="https://github.com/architect/tap-arc"&gt;&lt;code&gt;tap-arc&lt;/code&gt;&lt;/a&gt;. Heavily inspired by &lt;code&gt;tap-spec&lt;/code&gt; (a passing suite's output is nearly identical), &lt;code&gt;tap-arc&lt;/code&gt; is a minimal, streaming TAP reporter with useful expected vs. actual diffing. We're still improving the package, but it's definitely on par with &lt;code&gt;tap-spec&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feedback?
&lt;/h2&gt;

&lt;p&gt;I'm super interested in what others are doing in this realm. How are you testing cloud functions? What factors are important when selecting test utilities? Do you test in the same environment you're deploying to?&lt;/p&gt;

</description>
      <category>testing</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Shopify Webhooks with Begin</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Tue, 30 Nov 2021 17:13:53 +0000</pubDate>
      <link>https://forem.com/begin/shopify-webhooks-with-begin-lci</link>
      <guid>https://forem.com/begin/shopify-webhooks-with-begin-lci</guid>
      <description>&lt;p&gt;&lt;small&gt;Photo by &lt;a href="https://unsplash.com/photos/TxzjVxnWYq4"&gt;Tatiana Rodriguez&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;It’s always nice when data comes to you, and even better if your program can react in real-time. &lt;a href="https://shopify.dev/apps/webhooks"&gt;Shopify offers helpful webhooks&lt;/a&gt; for all sorts of store events. A developer just needs to set up a way to listen for those hooks and act on their data.&lt;/p&gt;

&lt;p&gt;Shopify does offer &lt;a href="https://shopify.dev/apps/webhooks/eventbridge"&gt;support for AWS EventBridge&lt;/a&gt; and &lt;a href="https://shopify.dev/apps/webhooks/google-cloud"&gt;GCP Pub/Sub&lt;/a&gt;. Unfortunately, the process for both requires some web console spelunking, subscribing to webhook topics programmatically via the Shopify API (eventually, a good idea), and then setting up a mechanism to actually do work on the data.&lt;/p&gt;

&lt;p&gt;Instead, let’s leverage the power and scale of AWS with the ease of &lt;a href="https://arc.codes"&gt;Architect&lt;/a&gt; and &lt;a href="https://begin.com"&gt;Begin&lt;/a&gt;. Without managing AWS projects, IAM roles, or connecting EventBridge to a series of Lambdas.&lt;/p&gt;

&lt;p&gt;Architect will help set up various HTTP routes (API Gateway endpoints backed by Lambda functions) to handle incoming webhook data. As an example, we’ll listen for customer updates and check if they're a VIP (10 or more lifetime orders).&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a new Architect app with &lt;code&gt;npm init&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init @architect shopify-webhooks
&lt;span class="nb"&gt;cd &lt;/span&gt;shopify-webhooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Architect has created the foundation for a new Arc project with a single HTTP endpoint. You can get a good view of what an Arc project is about by viewing &lt;a href="https://arc.codes/docs/en/get-started/project-manifest"&gt;the &lt;code&gt;./app.arc&lt;/code&gt; file&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add an HTTP function called "customer-update"
&lt;/h2&gt;

&lt;p&gt;Create a new entry in the app’s manifest. We'll use the "post" prefix to declare an endpoint that will respond to POST requests like webhooks from Shopify.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@app
shopify-webhooks

@http
get /
post /customer-update
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Architect's CLI can help by creating some scaffolding. Just run &lt;code&gt;arc create&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx arc create
&lt;span class="c"&gt;# ✓ Create Existing Architect project manifest found&lt;/span&gt;
&lt;span class="c"&gt;# ✓ Create Created new project files in src/http/post-customer_update/&lt;/span&gt;
&lt;span class="c"&gt;# ✓ Create Done!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Update the &lt;code&gt;customer-update&lt;/code&gt; handler function
&lt;/h2&gt;

&lt;p&gt;First, let's add the &lt;a href="https://arc.codes/docs/en/reference/runtime-helpers/no#%40architect%2Ffunctions"&gt;&lt;code&gt;@architect/functions&lt;/code&gt;&lt;/a&gt; helper.&lt;br&gt;&lt;br&gt;
This library has some utilities to help parse request bodies, handle asynchronous code, and more. It's also available in Ruby and Python. Yep, this entire workflow can be done with &lt;a href="https://arc.codes/docs/en/reference/configuration/function-config#runtime"&gt;alternative runtimes&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i @architect/functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll add some very simple code to &lt;code&gt;src/http/post-customer_update/index.js&lt;/code&gt; that will acknowledge a webhook request and decide what to log based on a customer's total order count.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;arc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@architect/functions&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// respond immediately&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; was updated`&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;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is a VIP 🎉`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; has &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orders_count&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; orders`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Test locally with &lt;a href="https://arc.codes/docs/en/reference/cli/sandbox"&gt;Architect Sandbox&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Architect has a Sandbox to help test our functions locally. Fire it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx arc sandbox
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Navigate to &lt;a href="http://localhost:3333"&gt;http://localhost:3333&lt;/a&gt; to confirm the server is running and see &lt;code&gt;get-index&lt;/code&gt; in action.&lt;/p&gt;

&lt;p&gt;Next, we can test our new endpoint locally with a curl POST:&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;-X&lt;/span&gt; POST http://localhost:3333/customer-update &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"id": 706405506930370084,"email": "bob@biller.com","orders_count": 0}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking the Sandbox logs, we can see the customer’s id was logged to the console.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;706405506930370084 was updated
706405506930370084 has 0 orders
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try sending the curl command again but set “orders_count” to 10. This time the customer is logged as a VIP 🎉&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;706405506930370084 was updated
706405506930370084 is a VIP 🎉
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deploy to Begin.com
&lt;/h2&gt;

&lt;p&gt;Let's get this to the cloud! Architect lets developers deploy directly to an existing AWS account if a local profile is set up.&lt;/p&gt;

&lt;p&gt;Instead, we'll make it even easier and deploy it to &lt;a href="https://begin.com"&gt;Begin.com&lt;/a&gt;. First, push the Arc project to a new GitHub repository. Then head to Begin.com (and make an account if needed) to create a new app.&lt;/p&gt;

&lt;p&gt;Select "Use an existing repo" and choose the GitHub repository we just created.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ebi1438m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8tgvaq0j74km9quh6ir8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ebi1438m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8tgvaq0j74km9quh6ir8.png" alt="select a repo at Begin.com" width="880" height="497"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;A staging build will kick off immediately. After that succeeds, go ahead and deploy to production. Once that's complete, you can click the production link to open the project's live version.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configure Shopify with Begin’s new URL
&lt;/h2&gt;

&lt;p&gt;Head over to the admin for the Shopify store you're working with. In store settings, under Notifications, add a new webhook. Set the topic to "Customer update" and use the production URL from Begin with the added &lt;code&gt;/customer-update&lt;/code&gt; path.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--j6dgqYia--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwiy0pp05r825l1jtv44.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--j6dgqYia--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nwiy0pp05r825l1jtv44.png" alt="create a Shopify webhook" width="880" height="567"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;h2&gt;
  
  
  Send a sample event from Shopify
&lt;/h2&gt;

&lt;p&gt;After saving the webhook in Shopify, click "Send test notification" next to the new event. This will POST a test payload to the live project deployed on Begin.&lt;/p&gt;

&lt;p&gt;From the Begin app dashboard, open the &lt;code&gt;customer-update&lt;/code&gt; function logs from the HTTP view to check the production logs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jxMB3C41--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5mi84onbvm9ioqofnsl4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jxMB3C41--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5mi84onbvm9ioqofnsl4.png" alt="Begin application logs" width="880" height="381"&gt;&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;Just like that, we're catching Shopify webhooks in production with AWS Lambdas, no complex set up required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;Okay, now what? Well, that's up to you and your app's needs. Here are some suggestions from my time in the Shopify ecosystem:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify &lt;a href="https://shopify.dev/apps/webhooks/https#verify-a-webhook"&gt;the webhook signature&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Save data to a database; like &lt;a href="https://docs.begin.com/en/data/begin-data/"&gt;Begin's built-in tables&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;POST data back to Shopify's API, like adding a &lt;code&gt;VIP&lt;/code&gt; &lt;a href="https://shopify.dev/api/admin-rest/2021-10/resources/customer"&gt;customer&lt;/a&gt; tag.&lt;/li&gt;
&lt;li&gt;Publish to an event queue (SNS) with &lt;a href="https://arc.codes/docs/en/reference/project-manifest/events"&gt;the @events pragma&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Handle Shopify's &lt;a href="https://shopify.dev/apps/webhooks/mandatory"&gt;mandatory webhooks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Reduce repetition by &lt;a href="https://arc.codes/docs/en/guides/developer-experience/sharing-code"&gt;sharing code&lt;/a&gt; between functions.&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>aws</category>
      <category>shopify</category>
      <category>webhooks</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Inventory of Developer Discord Sidebars</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Sat, 04 Sep 2021 17:51:13 +0000</pubDate>
      <link>https://forem.com/tbeseda/inventory-of-developer-discord-sidebars-12o4</link>
      <guid>https://forem.com/tbeseda/inventory-of-developer-discord-sidebars-12o4</guid>
      <description>&lt;p&gt;I've been doing some research on developer-centric Discord server setups. I had grabbed a snapshot of several servers I'm currently in, so I compiled them into a single image here in case it's useful to others.&lt;/p&gt;

&lt;p&gt;By no means exhaustive, scientific, or in any particular order:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Wmn6qeXM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://user-images.githubusercontent.com/15697/132104024-bc89f7a9-f142-4437-bf5e-7cc0542c54a5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Wmn6qeXM--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://user-images.githubusercontent.com/15697/132104024-bc89f7a9-f142-4437-bf5e-7cc0542c54a5.png" alt="developer discord sidebars"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://user-images.githubusercontent.com/15697/132104024-bc89f7a9-f142-4437-bf5e-7cc0542c54a5.png"&gt;full res&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Technical Writing Highlights from Large Orgs</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Sun, 01 Aug 2021 18:38:21 +0000</pubDate>
      <link>https://forem.com/tbeseda/technical-writing-highlights-from-large-orgs-5d2j</link>
      <guid>https://forem.com/tbeseda/technical-writing-highlights-from-large-orgs-5d2j</guid>
      <description>&lt;p&gt;I've been reading through the style guides offered by larger tech companies. These summaries are a great place to start.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://developers.google.com/style/highlights"&gt;&lt;strong&gt;Google&lt;/strong&gt; developer documentation style guide &lt;em&gt;Highlights&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.microsoft.com/en-us/style-guide/top-10-tips-style-voice"&gt;&lt;strong&gt;Microsoft&lt;/strong&gt; &lt;em&gt;Top 10 tips for Microsoft style and voice&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.splunk.com/Documentation/StyleGuide/current/StyleGuide/AwordaboutSplunkdocs"&gt;&lt;strong&gt;Splunk&lt;/strong&gt; &lt;em&gt;A word about Splunk docs&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.openstack.org/doc-contrib-guide/writing-style/general-writing-guidelines.html"&gt;&lt;strong&gt;OpenStack&lt;/strong&gt; &lt;em&gt;General writing guidelines&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ibm.com/brand/systems/developer/brand/voice"&gt;&lt;strong&gt;IBM&lt;/strong&gt; Developer Experience Guide: &lt;em&gt;Voice&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://redhat-documentation.github.io/supplementary-style-guide/"&gt;&lt;strong&gt;Red Hat&lt;/strong&gt; &lt;em&gt;Style guide for product documentation&lt;/em&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.gitlab.com/13.12/ee/development/documentation/styleguide/"&gt;&lt;strong&gt;Gitlab&lt;/strong&gt; Style guide&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My takeaway: concise, conversational, active voice, you can use second person, and Oxford commas FTW.&lt;/p&gt;

</description>
      <category>documentation</category>
      <category>technical</category>
      <category>writing</category>
    </item>
    <item>
      <title>CLI for Managing Shopify App Webhooks</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Sat, 10 Jul 2021 21:27:59 +0000</pubDate>
      <link>https://forem.com/tbeseda/cli-for-managing-shopify-app-webhooks-2n0a</link>
      <guid>https://forem.com/tbeseda/cli-for-managing-shopify-app-webhooks-2n0a</guid>
      <description>&lt;p&gt;&lt;a href="https://www.npmjs.com/package/shopify-webhook-commander" rel="noopener noreferrer"&gt;&lt;code&gt;shopify-webhook-commander&lt;/code&gt;&lt;/a&gt; is a CLI to create, list, and delete Shopify Admin App webhook subscriptions.&lt;/p&gt;

&lt;p&gt;Building out a new admin app often requires setting up listeners for Shop events but building out controllers and UI to manage webhook subscriptions isn't always at the top of the to-do list.&lt;/p&gt;

&lt;p&gt;This CLI will let developers manage subscriptions from the terminal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5vicgwjwl83u841g4ui.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr5vicgwjwl83u841g4ui.gif" alt="shopify-webhook-commander demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;🚫 This package is under active development; installer beware.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npm i -g shopify-webhook-commander
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Note about auth tokens
&lt;/h2&gt;

&lt;p&gt;This tool uses a single token to authenticate; this is easiest with a Shopify "Private" Admin App. Set the config secret to the private app password. &lt;em&gt;There is no OAuth flow to help with "Custom" Partner apps.&lt;/em&gt;&lt;br&gt;
You could provide an &lt;code&gt;accessToken&lt;/code&gt; acquired from a Custom app's authentication flow.&lt;br&gt;
shopify-webhook-commander doesn't really help for "Public" apps that typically manage webhooks for each shop install programatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roadmap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;tests 😬&lt;/li&gt;
&lt;li&gt;replace Apollo client with a slimmer GQL lib&lt;/li&gt;
&lt;li&gt;integrate &lt;code&gt;@shopify/shopify-api&lt;/code&gt; types -- gotta get rid of those &lt;code&gt;any&lt;/code&gt; types&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;webhooks:update&lt;/code&gt; command&lt;/li&gt;
&lt;li&gt;autocomplete webhook topics&lt;/li&gt;
&lt;li&gt;auth loop for custom apps&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8uy11d6lz2epdf1jx7cz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8uy11d6lz2epdf1jx7cz.png" alt="some cli typescript"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>typescript</category>
      <category>shopify</category>
      <category>graphql</category>
    </item>
    <item>
      <title>Deploying Nodewood</title>
      <dc:creator>Taylor Beseda</dc:creator>
      <pubDate>Tue, 08 Jun 2021 03:54:02 +0000</pubDate>
      <link>https://forem.com/tbeseda/deploying-nodewood-38ag</link>
      <guid>https://forem.com/tbeseda/deploying-nodewood-38ag</guid>
      <description>&lt;p&gt;[&lt;em&gt;This was originally  &lt;a href="https://github.com/Nodewood/issues/discussions/3"&gt;posted to a Github discussion on the Nodewood repository&lt;/a&gt; ; adding here for posterity.&lt;/em&gt;]&lt;/p&gt;

&lt;p&gt;I recently deployed a vanilla instance of &lt;a href="https://nodewood.com/"&gt;Nodewood&lt;/a&gt;, a JavaScript SaaS Starter Kit, to the cloud. I picked Render.com but this might help with other platforms (PaaS).&lt;/p&gt;

&lt;p&gt;Things I set up before deploying:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A fresh Nodewood project running locally according to the Guide (great docs!)&lt;/li&gt;
&lt;li&gt;My Nodewood project pushed to private Github repo (with changes outlined below)&lt;/li&gt;
&lt;li&gt;Stripe account (activated, so I have production API keys)&lt;/li&gt;
&lt;li&gt;Render account linked to my Github account&lt;/li&gt;
&lt;li&gt;A Postgres db from Render (an addon for $7/mo)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This looks like a lot of work, but it's fairly quick-n-easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project changes
&lt;/h2&gt;

&lt;h4&gt;
  
  
  &lt;code&gt;knexfile.js&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;❗ Add &lt;code&gt;knexfile.js&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt; so that it won't be a part of the production codebase. Knex configuration specific to production will be added to the Render service configuration (covered below).&lt;br&gt;&lt;br&gt;
Run &lt;code&gt;&amp;gt; git rm knexfile.js&lt;/code&gt; if it has already been added to the repo.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;code&gt;package.json&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;Add some helpful commands to &lt;code&gt;scripts&lt;/code&gt; for production use on Render:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+    "production:build": "yarn &amp;amp;&amp;amp; yarn production:migrate &amp;amp;&amp;amp; yarn production:stripe-sync &amp;amp;&amp;amp; yarn production:build-ui",
+    "production:migrate": "knex migrate:latest --env production",
+    "production:stripe-sync": "npx @nodewood/cli stripe:sync --no-confirm",
+    "production:build-ui": "NODE_ENV=production yarn build-ui",
+    "production:start": "node app/api/api.js",
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are simple helpers that can be edited later if needed. Render will be configured to run these as a part of the build and deploy process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Render Service Setup
&lt;/h2&gt;

&lt;p&gt;Create a new "Web Service" linked to the (private) Github repo. $7/mo&lt;/p&gt;

&lt;h3&gt;
  
  
  Settings
&lt;/h3&gt;

&lt;p&gt;❗ Change "Environment" option to "Node". Render will autodetect "Docker" but in this case it would add some unneeded overhead.&lt;/p&gt;

&lt;p&gt;Build command: &lt;code&gt;yarn production:build&lt;/code&gt; the custom script added to package.json that will run with each deploy&lt;/p&gt;

&lt;p&gt;Start command: &lt;code&gt;yarn production:start&lt;/code&gt; run the API&lt;/p&gt;

&lt;h3&gt;
  
  
  Advanced Settings
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Environment variable:
&lt;/h4&gt;

&lt;p&gt;key: &lt;code&gt;NODE_ENV&lt;/code&gt;, value: 'production' -- just to be sure our code knows where it's at.&lt;/p&gt;

&lt;h4&gt;
  
  
  3 Secret files:
&lt;/h4&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;.env&lt;/code&gt;: copied from local project with updated &lt;code&gt;DB_&lt;/code&gt; variables for the Render database and updated production &lt;code&gt;pk_&lt;/code&gt; and &lt;code&gt;sk_&lt;/code&gt; keys from Stripe&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.nodewood.js&lt;/code&gt;: copied exactly from local project 👍🏻 &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;knexfile.js&lt;/code&gt;: with only a production object containing the Render database details. Looks similar to:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;postgresql&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;:&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;Hostname from Render database&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;myapp&lt;/span&gt;&lt;span class="dl"&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;myapp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;generatedpassword&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;migrations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;knex_migrations&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;stub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;migrations.stub&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;directory&lt;/span&gt;&lt;span class="p"&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;./wood/migrations&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;./app/migrations&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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deploy 🚀
&lt;/h3&gt;

&lt;p&gt;Commit and push your main git branch, Render will handle the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using your Nodewood App
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Visit your Nodewood app (probably a .onrender.com URL) to see the static content (from &lt;code&gt;./www/dist/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Sign up for an account&lt;/li&gt;
&lt;li&gt;Edit the new user record in the database so that &lt;code&gt;email_confirmed&lt;/code&gt; = TRUE and &lt;code&gt;account_type&lt;/code&gt; = "admin". (I use Postico.app on my Mac to connect to the Render database to do this)&lt;/li&gt;
&lt;li&gt;Visit the admin section of your Nodewood application in the browser!&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is as far as I've gotten so far, but from here I can just focus on my idea and not worry about deploying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes/Ideas:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;I didn't check if Knex could be configured from .env or similar -- this would eliminate the need for a production &lt;code&gt;./knexfile.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;No mailer was set up yet.&lt;/li&gt;
&lt;li&gt;It's probably possible to deploy with Docker on Render, but that kind of defeats the purpose here 😄 &lt;/li&gt;
&lt;li&gt;I didn't check how many Postgres connections are allowed by Render's db service. So I may need to update the production Knex &lt;code&gt;pool&lt;/code&gt; configuration&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>node</category>
      <category>devops</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
