<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Austin Gil</title>
    <description>The latest articles on DEV Community by Austin Gil (@austingil).</description>
    <link>https://dev.to/austingil</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%2F183813%2F679c6a1a-452f-452b-85a6-e46a204bcd64.png</url>
      <title>DEV Community: Austin Gil</title>
      <link>https://dev.to/austingil</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/austingil"/>
    <language>en</language>
    <item>
      <title>The C̶a̶k̶e̶ User Location is a Lie!!!</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Wed, 31 Jul 2024 18:29:32 +0000</pubDate>
      <link>https://dev.to/austingil/the-cake-user-location-is-a-lie-340l</link>
      <guid>https://dev.to/austingil/the-cake-user-location-is-a-lie-340l</guid>
      <description>&lt;p&gt;I recently sat in on a discussion about programming based on user location. Folks that are way smarter than me covered technical limitations, legal concerns, and privacy rights. It was nuanced, to say the least.&lt;/p&gt;

&lt;p&gt;So, I thought I’d share some details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Location, Location, Location
&lt;/h2&gt;

&lt;p&gt;There are several common examples when you may want to add location-based logic to your app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You want to set the language or currency of your app based on the region.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You’re offering discounts to people in a given country.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You have a store locator that should show user’s their nearest location.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Your weather app relies on a location before it can offer any sort of data.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You want to&lt;/strong&gt; &lt;a href="https://en.wikipedia.org/wiki/Geo-fence" rel="noopener noreferrer"&gt;&lt;strong&gt;geofence&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;your app for legal reasons (eg. cookie banners).&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are just a few use-cases. There are plenty more, but from these, we can identify some common themes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Presentation/User experience&lt;/strong&gt;: Using location information to improve or streamline the user experience.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Function/Logic&lt;/strong&gt;: The application’s business logic changes based on location.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Policy/Compliance&lt;/strong&gt;: You have legal requirements to include or exclude functionality.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s not always this clear-cut. There is overlap in some cases, but it’s important to keep these distinctions in mind, because getting it wrong has different levels of severity. Showing the wrong currency is not as bad as miscalculating tax rates, which is still not as bad as violating an embargo, for example.&lt;/p&gt;

&lt;p&gt;With that in mind, let’s look at the options we have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting User Location
&lt;/h2&gt;

&lt;p&gt;There are four ways I know of to access the user’s location, each with their pros and cons.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User reporting&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Device heuristics&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IP Address&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edge compute&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Getting User Location From the User
&lt;/h3&gt;

&lt;p&gt;This is when you have a form on your website that explicitly asks a user where they are. It may offer user experience improvements like auto-completing an address, but ultimately, you are taking the user at their word.&lt;/p&gt;

&lt;p&gt;This method has the benefits of being easy to get started (an &lt;a href="https://austingil.com/how-to-build-html-forms-right-semantics/" rel="noopener noreferrer"&gt;HTML form&lt;/a&gt; will do), provides as reliable information as the user allows, is flexible to support different locations.&lt;/p&gt;

&lt;p&gt;The most obvious down-side is that it may not be accurate if the user mistypes or omits information. Furthermore, it’s very easy for a user to provide false information. This can be allowed in some cases, and a big mistake in others.&lt;/p&gt;

&lt;p&gt;Take this, for example.&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%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-104.png%2520align%3D" 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%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-104.png%2520align%3D" alt="Screenshot of a form asking for the user's location, but the user put, "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a legitimate place in New Jersey…it’s ok to laugh. I actually went down a bit of a rabbit hole “researching” real places with funny names and spent way too much time, but I came across some real gems: &lt;a href="https://www.yelp.com/city/monkeys-eyebrow-ky-us" rel="noopener noreferrer"&gt;Monkey’s Eyebrow – Kentucky&lt;/a&gt;, &lt;a href="https://www.summitpost.org/big-butt-mountain/853145" rel="noopener noreferrer"&gt;Big Butt Mountain – North Carolina&lt;/a&gt;, &lt;a href="https://www.ci.unalaska.ak.us/" rel="noopener noreferrer"&gt;Unalaska – Alaska&lt;/a&gt;, &lt;a href="https://www.bestplaces.net/city/arizona/why" rel="noopener noreferrer"&gt;Why – Arizona&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Whynot,_North_Carolina" rel="noopener noreferrer"&gt;Whynot – North Carolina&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Full list of real places with funny names&lt;/p&gt;
&lt;ul&gt;

&lt;li&gt;Accident, Maryland&lt;/li&gt;

&lt;li&gt;Bacon Level, Alabama&lt;/li&gt;

&lt;li&gt;Bald Head, Maine&lt;/li&gt;

&lt;li&gt;Bat Cave, North Carolina&lt;/li&gt;

&lt;li&gt;Batman&lt;/li&gt;

&lt;li&gt;Beaverlick&lt;/li&gt;

&lt;li&gt;Bell End&lt;/li&gt;

&lt;li&gt;Bigfoot, Texas&lt;/li&gt;

&lt;li&gt;Bitter End, Tennessee&lt;/li&gt;

&lt;li&gt;Booger Hole, West Virginia&lt;/li&gt;

&lt;li&gt;Boring, Oregon (I’ve been there!)&lt;/li&gt;

&lt;li&gt;Breeding, Kentucky&lt;/li&gt;

&lt;li&gt;Burnout, Alabama&lt;/li&gt;

&lt;li&gt;Burnt Store, Florida&lt;/li&gt;

&lt;li&gt;Butternuts, New York&lt;/li&gt;

&lt;li&gt;Butthole Lane&lt;/li&gt;

&lt;li&gt;Carefree, Arizona&lt;/li&gt;

&lt;li&gt;Center of the World, Ohio&lt;/li&gt;

&lt;li&gt;Cheesequake, New Jersey&lt;/li&gt;

&lt;li&gt;Chicken, Alaska&lt;/li&gt;

&lt;li&gt;Chugwater, Wyoming&lt;/li&gt;

&lt;li&gt;Cookietown, Oklahoma&lt;/li&gt;

&lt;li&gt;Correct, Indiana&lt;/li&gt;

&lt;li&gt;Dick’s Knob&lt;/li&gt;

&lt;li&gt;Ding Dong, Texas&lt;/li&gt;

&lt;li&gt;Disappointment Islands&lt;/li&gt;

&lt;li&gt;Earth, Texas&lt;/li&gt;

&lt;li&gt;Eggnog, Utah&lt;/li&gt;

&lt;li&gt;Fifty-Six, Arkansas&lt;/li&gt;

&lt;li&gt;Funk, Nebraska&lt;/li&gt;

&lt;li&gt;Greasy Corner, Arkansas&lt;/li&gt;

&lt;li&gt;Hell, Michigan&lt;/li&gt;

&lt;li&gt;Hot Coffee, Mississippi&lt;/li&gt;

&lt;li&gt;Humptulips, Washington&lt;/li&gt;

&lt;li&gt;Idiotville&lt;/li&gt;

&lt;li&gt;Imalone, Wisconsin&lt;/li&gt;

&lt;li&gt;Intercourse, Pennsylvania&lt;/li&gt;

&lt;li&gt;Ketchuptown, South Carolina&lt;/li&gt;

&lt;li&gt;Kickapoo, Kansas&lt;/li&gt;

&lt;li&gt;Looneyville, Texas&lt;/li&gt;

&lt;li&gt;Moose Factory&lt;/li&gt;

&lt;li&gt;Mosquitoville, Vermont&lt;/li&gt;

&lt;li&gt;Neutral, Kansas&lt;/li&gt;

&lt;li&gt;New Erection&lt;/li&gt;

&lt;li&gt;No Name, Colorado&lt;/li&gt;

&lt;li&gt;Normal, Illinois&lt;/li&gt;

&lt;li&gt;Nothing, Arizona&lt;/li&gt;

&lt;li&gt;Peculiar, Missouri&lt;/li&gt;

&lt;li&gt;Pee Pee, Ohio&lt;/li&gt;

&lt;li&gt;Red Shirt, South Dakota&lt;/li&gt;

&lt;li&gt;Sandwich, Massachusetts&lt;/li&gt;

&lt;li&gt;Satan’s Kingdom&lt;/li&gt;

&lt;li&gt;Scratch My Arse Rock&lt;/li&gt;

&lt;li&gt;Slaughterville, Oklahoma&lt;/li&gt;

&lt;li&gt;Stupid Lake&lt;/li&gt;

&lt;li&gt;Sweet Lips, Tennessee&lt;/li&gt;

&lt;li&gt;Worms, Nebraska&lt;/li&gt;

&lt;li&gt;Zzyzx, California&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Anyway, if you decide to take this approach, it’s a good idea to either use a form control with pre-selected options (select or radio), or integrate some sort of auto-complete (location API). This provides a better use-experience, and usually leads to more complete/reliable/accurate data.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting User Location From the Device
&lt;/h3&gt;

&lt;p&gt;Modern devices like smartphones and laptops have access to their location information through GPS, Wi-Fi data, cell towers, and IP address. As web developers, we don’t get direct access to this information, for security reasons, but there are some things we can do.&lt;/p&gt;

&lt;p&gt;The first thing that comes to mind is the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Geolocation" rel="noopener noreferrer"&gt;Geolocation API&lt;/a&gt; built into the browser. This provides a way for websites to request access to the user’s location with the &lt;code&gt;getCurrentPosition&lt;/code&gt; method:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;navigator.geolocation.getCurrentPosition&lt;span class="o"&gt;(&lt;/span&gt;data &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  console.log&lt;span class="o"&gt;(&lt;/span&gt;data&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The function provides you with a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPosition" rel="noopener noreferrer"&gt;&lt;code&gt;GeolocationPosition&lt;/code&gt;&lt;/a&gt; object containing latitude, longitude, and other information:&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;{&lt;/span&gt;
  coords: &lt;span class="o"&gt;{&lt;/span&gt;
    accuracy: 1153.4846436496573
    altitude: null
    altitudeAccuracy: null
    heading: null
    latitude: 28.4885376
    longitude: 49.6407936
    speed: null
  &lt;span class="o"&gt;}&lt;/span&gt;,
  timestamp: 1710198149557
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Great! Just one problem:&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%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-103.png%2520align%3D" 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%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-103.png%2520align%3D" alt="Screenshot of the browser popup when a website asks to access your location."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first time a website tries to use the Geolocation API, the user will be prompted with a request to share their information.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Best case: the user understands the extra step and accepts.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mid case: the user gets annoyed and has a 50/50 chance of accepting or denying.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Worst case: the user is paranoid about government surveilance, assumes worst intentions, and never comes back to your app (this is me).&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When using an API that requires a user verification, it’s often a good idea to let the user know ahead of time to expect the popup, and only trigger it right when you need it. In other words, don’t request access as soon as your app loads. Wait until the user has focused on the location input field, for example.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting User Location From Their IP Address
&lt;/h3&gt;

&lt;p&gt;In case you’re not familiar, an &lt;a href="https://en.wikipedia.org/wiki/IP_address" rel="noopener noreferrer"&gt;IP address&lt;/a&gt; looks like this, 192.0.2.1. They are used to uniquely identify and locate devices in a network. This is how computers communicate over the internet, and each packet of data contains information about the IP address of the sender. Your home internet modem is a good example of a device in a network with an IP address.&lt;/p&gt;

&lt;p&gt;The relevant thing to note is that you can get location information from an IP address. Each chunk of numbers (separated by periods) represents a subnet from broader to finer scope. You can think of it as going from country to ISP, to region, to user. It doesn’t get fine enough to know someone’s specific address, but it’s possible to get the city or zip code.&lt;/p&gt;

&lt;p&gt;Here are two great resources if you want to know more about how this works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Internet_geolocation" rel="noopener noreferrer"&gt;&lt;strong&gt;Wikipidia’s Internet geolocation page&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.howtogeek.com/devops/how-to-get-location-information-from-an-ip-address/" rel="noopener noreferrer"&gt;&lt;strong&gt;How-To Geek’s article, How to Get Location Information from an IP Address&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For &lt;a href="https://austingil.com/category/development/javascript/" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; developers like myself, you can access the remote IP in &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; with &lt;a href="https://nodejs.org/dist/latest/docs/api/net.html#net_socket_remoteaddress" rel="noopener noreferrer"&gt;&lt;code&gt;response.socket.remoteAddress&lt;/code&gt;&lt;/a&gt;. And note that you are not getting the user’s IP, technically. You’re getting the IP address for the user’s connection (and anyone else on their connection), by way of their modem and ISP.&lt;/p&gt;

&lt;p&gt;Internet user -&amp;gt; ISP -&amp;gt; IP address.&lt;/p&gt;

&lt;p&gt;An IP address alone is not enough to know where a user is coming from. You’ll need to look up the IP address subnets against a database of known subnet locations. It usually doesn’t make sense to maintain your own list. Instead, you can &lt;a href="https://lite.ip2location.com/ip2location-lite" rel="noopener noreferrer"&gt;download an existing one&lt;/a&gt;, or ping a 3rd party service to look it up.&lt;/p&gt;

&lt;p&gt;For basic needs, &lt;a href="https://lite.ip2location.com/" rel="noopener noreferrer"&gt;ip2location.com&lt;/a&gt; and &lt;a href="https://tools.keycdn.com/geo" rel="noopener noreferrer"&gt;KeyCDN&lt;/a&gt; offer free, limited options. For apps that rely heavily on determining geolocation from IP addresses or need a higher level of accuracy, you’ll want something more robust.&lt;/p&gt;

&lt;p&gt;So now, we have a solution that requires no work from the user, and has a pretty high level of accuracy. Pretty high accuracy is not a guarantee that the user’s IP address is accurate, as we will see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting User Location From Edge Compute
&lt;/h3&gt;

&lt;p&gt;I’ve written &lt;a href="https://austingil.com/category/development/edge-compute/" rel="noopener noreferrer"&gt;several articles about edge compute&lt;/a&gt; in the past, so I won’t go too deep, but edge compute is a way to run dynamic, server-side code against a user’s request from the nearest server. It works by routing all requests through a network of globally distributed servers, or nodes, and allowing the network to choose the nearest node to the user.&lt;/p&gt;

&lt;p&gt;The great thing about edge compute is that the platforms provide you with user location information without the need to ask the user for permission or look up an IP address. It can provide this information because every node knows where it lives.&lt;/p&gt;

&lt;p&gt;Akamai’s edge compute platform, &lt;a href="https://www.akamai.com/products/serverless-computing-edgeworkers" rel="noopener noreferrer"&gt;EdgeWorkers&lt;/a&gt;, gives you access to a &lt;a href="https://techdocs.akamai.com/edgeworkers/docs/request-object" rel="noopener noreferrer"&gt;request object&lt;/a&gt; with a &lt;a href="https://techdocs.akamai.com/edgeworkers/docs/user-location-object" rel="noopener noreferrer"&gt;&lt;code&gt;userLocation&lt;/code&gt;&lt;/a&gt; property. This property is a User Location Object that looks something like this:&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;{&lt;/span&gt;
  areaCodes: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"617"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,
  bandwidth: &lt;span class="s2"&gt;"257"&lt;/span&gt;,
  city: &lt;span class="s2"&gt;"CAMBRIDGE"&lt;/span&gt;,
  continent: &lt;span class="s2"&gt;"NA"&lt;/span&gt;, // North America
  country: &lt;span class="s2"&gt;"US"&lt;/span&gt;,
  dma: &lt;span class="s2"&gt;"506"&lt;/span&gt;,
  fips: &lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"25"&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;,
  latitude: &lt;span class="s2"&gt;"42.364948"&lt;/span&gt;,
  longitude: &lt;span class="s2"&gt;"-71.088783"&lt;/span&gt;,
  networkType: &lt;span class="s2"&gt;"mobile"&lt;/span&gt;,
  region: &lt;span class="s2"&gt;"MA"&lt;/span&gt;,
  timezone: &lt;span class="s2"&gt;"GMT"&lt;/span&gt;,
  zipCode: &lt;span class="s2"&gt;"02114+02134+02138-02142+02163+02238"&lt;/span&gt;,
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So now we have a reliable source of location information with little effort, the only issue is that it’s not &lt;strong&gt;technically&lt;/strong&gt; the user’s location. The User Location Object actually represents the edge node that received the user’s request. This will be the closest node to the user; likely in the same area. This is a subtle distinction, but depending on your needs, it can make a big difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is Why We Can’t Have Nice Things!
&lt;/h2&gt;

&lt;p&gt;So we’ve covered some options along with their benefits and caveats, but here’s the real kicker. None of the options we’ve looked at can be trusted.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can’t trust the user
&lt;/h3&gt;

&lt;p&gt;As mentioned above, we can’t trust users to always be honest and put in their actual location. And even if we could, they could make mistakes. And even if they don’t some data can be mistaken. For example, if I ask someone for their city, and they put “Portland” how can I be certain they mean Portland, OR (the best Portland), and not one of the 18+ others (in the US, alone).&lt;/p&gt;

&lt;h3&gt;
  
  
  Can’t trust the device
&lt;/h3&gt;

&lt;p&gt;The first issue with things like the Geolocation API is that the user can just disallow using it. To which you may respond, “Fine, they can’t use my app then.” But this also fails to address another issue, which is the fact that the Geolocation API information can actually be overwritten by the user in their browser settings. And it’s not even that hard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can’t trust the IP address
&lt;/h3&gt;

&lt;p&gt;I’m not sure if it’s possible to spoof an IP address for the computer that is connecting to your website, but it’s pretty easy for a user to route their request through a proxy client. Commonly, this is referred to as a &lt;a href="https://en.wikipedia.org/wiki/Virtual_private_network" rel="noopener noreferrer"&gt;Virtual Private Network&lt;/a&gt;, or VPN. The user connects to a VPN, their request goes to the VPN first, then the VPN connects to your website. As a result, the IP address you see is the VPN’s, not the user’s. This means any location data you get will be for the VPN, and not the user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can’t trust edge compute
&lt;/h3&gt;

&lt;p&gt;Edge compute offers reliable information, but that information is the location of the edge node, and not the actual user. Often, they can be close enough, but it’s possible that the user lives near the border of one region and their nearest edge node is on the other side of that border. What happens if you have distinct behavior based on those regional differences? Also, edge compute is not free from the same VPN issues as IP addresses. With Akamai’s &lt;a href="https://www.akamai.com/blog/performance/act-against-geopiracy-with-enhanced-proxy-detection" rel="noopener noreferrer"&gt;Enhanced Proxy Detection&lt;/a&gt;, you can identify &lt;strong&gt;if&lt;/strong&gt; someone is using a VPN, but you still can’t access their original IP address.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Can We Do About It?
&lt;/h3&gt;

&lt;p&gt;So, there are a lot of ways to get location information, but none of them are entirely reliable. In fact, browser extensions can make it trivial for users to circumvent our efforts. Does that mean we should give up?&lt;/p&gt;

&lt;p&gt;No!&lt;/p&gt;

&lt;p&gt;I want to leave you better informed and prepared. So let’s look at some examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Content Translation
&lt;/h3&gt;

&lt;p&gt;Say we have a website written in English, but also supports other languages. We’d like to improve the user experience by loading the local language of the user.&lt;/p&gt;

&lt;p&gt;How should we treat users from Belgium, where they speak Dutch (Flemish), French, and German? Should we default to the most common language (Dutch)? Should default to the default website language (English)?&lt;/p&gt;

&lt;p&gt;For the first render of the page, I think it’s safe to either use the default language or the best guess, but the key thing is to let the user decide which is best for them (maybe they only speak French) and honor their decision on subsequent visits.&lt;/p&gt;

&lt;p&gt;It could look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User requests the website.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Request passes through edge compute to determine it’s coming from Belgium.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edge compute looks for the language preference from an HTTP cookie.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If the cookie is present, use the preferred language.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If the cookie is not present, use the English or Dutch version.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;In the website, provide the user with a list of predefined, supported languages (maybe using a&lt;/strong&gt; &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; field).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When the user selects a language preference, store the value in a cookie for future sessions.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this scenario, we combine &lt;strong&gt;edge compute&lt;/strong&gt; with &lt;strong&gt;user reporting&lt;/strong&gt; to get location information to improve the experience. I don’t think it makes sense to use the Geolocation API at all. There is a risk of showing the wrong language, but the cost is low. The website works even if the location information is wrong or missing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Weather App
&lt;/h3&gt;

&lt;p&gt;In this example, we have an application that shows the weather information based on location. In this case, the app &lt;strong&gt;requires&lt;/strong&gt; the location information in order to work. How else can we show the weather?&lt;/p&gt;

&lt;p&gt;In this scenario, it’s still safe to assume the user’s location on first load. We can pull that information either from edge compute, or from the IP address, then show (what we think is) the user’s local weather. In addition to that, because the website’s main focus relies on location, we can use the Geolocation API to ask for more accurate data. We’ll also want to offer a flexible user reporting option in case the user want information for a different location. For that, a search input with auto-complete to fill in the location information with as much detail as possible. How you handle future visits may vary. You could always default to the “local” weather, or you could remember the location from the previous visit.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;User requests the website.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;On the first request, start the app assuming location information from edge compute or IP address.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;On the first client-side load, initiate the Geolocation API and update the information if necessary.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You can store location information in a cookie for future loads.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For other location searches, provide a flexible input that auto-completes location information and updates the app on submission.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important thing to note here is that the app doesn’t actually care about where the &lt;strong&gt;user&lt;/strong&gt; is located. We just care about having &lt;strong&gt;a location&lt;/strong&gt;. User reported location (search) takes precedence over a location found in a cookie, edge compute, or IP address.&lt;/p&gt;

&lt;p&gt;Due to the daily change in weather, it’s also worth considering caching strategy and whether the app should be primarily server-rendered or client-rendered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Store Locator
&lt;/h3&gt;

&lt;p&gt;Imagine you run a brick-and-mortar business with multiple locations. You might show your product catalog and inventory online, but a good practice is to offer up-to-date information about the in-store inventory. For that, you would need to know which store to show inventory for, and for the best user experience, it should be the store closest to the user.&lt;/p&gt;

&lt;p&gt;Once again, it makes sense to predict the user’s location using edge compute or IP address. Then, you also want to offer a flexible input that allows the user to put in their location information, but any auto-complete should be limited to the list of stores, sorted by proximity. It’s also good to initiate the Geolocation API.&lt;/p&gt;

&lt;p&gt;The difference between this example and the last is that the main purpose of the site is &lt;strong&gt;not&lt;/strong&gt; location dependent. Therefore, you should wait until the user has interacted with the location-dependent feature. In other words, only ask the user for their location when they’ve focused on the store locator field.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regional Pricing
&lt;/h3&gt;

&lt;p&gt;This one’s a little tricky, but how would you handle charging different prices based on the user’s location? For example, some airlines and hotels have been reported to have higher prices for users booking from one region vs. another.&lt;/p&gt;

&lt;p&gt;Ethics aside, this is a question about profits, which is highly impactful. So, you probably don’t want to allow users to easily change their prices through user-reported location information.&lt;/p&gt;

&lt;p&gt;In this case, you’d probably only use edge compute or IP address. It’s possible for users to get around it with a VPN, but it’s probably the best you could do. If you’re really concerned about avoiding scammers, you could use Akamai’s &lt;a href="https://www.akamai.com/blog/performance/act-against-geopiracy-with-enhanced-proxy-detection" rel="noopener noreferrer"&gt;Enhanced Proxy Detection&lt;/a&gt; and try blocking requests from VPN users, but that could lead to a no-sale instead of a discounted sale. Up to you.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cookie Banners
&lt;/h3&gt;

&lt;p&gt;This last example focuses more on the legal compliance side, so I’ll start with a small disclaimer: &lt;strong&gt;I AM NOT A LAWYER!!!&lt;/strong&gt; This is a hypothetical example and should not be taken as legal advice.&lt;/p&gt;

&lt;p&gt;In 2016, the European Union passed the &lt;a href="https://gdpr-info.eu/" rel="noopener noreferrer"&gt;General Data Protection Regulation (GDPR)&lt;/a&gt;. It’s a law that protects the privacy of internet users in the EU, and it applies to companies that offer goods or services to individuals in the EU, even if the company is based elsewhere.&lt;/p&gt;

&lt;p&gt;It has a lot of requirements for website owners, but the one I’ll focus on is the blight of cookie banners we now see everywhere online.&lt;/p&gt;

&lt;p&gt;I’ll avoid discussing privacy issues, whether cookie banners are right or wrong, the effectiveness or ineffectiveness of them, or if there is a better approach. Instead, I’ll just say that you may want to only show cookie banners when you are legally required, and avoid them otherwise.&lt;/p&gt;

&lt;p&gt;Once again, knowing the user’s location is pretty important. This is very similar to the previous case, and the implementation is similar too. The main difference is the severity of getting it wrong, and therefore the level of effort to get it right.&lt;/p&gt;

&lt;p&gt;Cookie banners might be the most ubiquitous example of how legislation and user location can impact a website, but if you’re looking for the most powerful, it’s probably &lt;a href="https://en.wikipedia.org/wiki/Great_Firewall" rel="noopener noreferrer"&gt;The Great Firewall of China&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Alright, hopefully this long and windy road has brought us all to the same place: The magical land of nuance.&lt;/p&gt;

&lt;p&gt;We still didn’t touch on a couple of other challenges:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What happens when a user changes their location mid-session?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What happens if there time zones are involved?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How do you report location information for disputed territories?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still, I hope you found it useful in learning how user location is determined, what challenges it faces, and some ways you might approach various scenarios. Unfortunately, there is no one right way to approach location data. Some scenarios are better suited for user reporting, some are better for device heuristics, and some are better for edge compute or IP address. In most cases, it’s some sort of combination.&lt;/p&gt;

&lt;p&gt;The important things you need to ask yourself are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do you need the user’s location or just any location?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How accurate does the data need to be?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Is it OK if the user location is falsified?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also legal compliance, regulations, functionality, is 95% reliable ok?&lt;/p&gt;

&lt;p&gt;If any of your location logic is for legal reasons, you’ll want to take steps to protect yourself. Account for data privacy laws like &lt;a href="https://www.oag.ca.gov/privacy/ccpa" rel="noopener noreferrer"&gt;CCPA&lt;/a&gt; and GDPR. Include messaging in your terms of service to disallow bad behavior. These are some things to consider, but I’m no lawyer. Consult your legal team.&lt;/p&gt;

&lt;p&gt;That’s all I have to say about that, but if you’re interested in cloud computing, new customers can sign up at &lt;a href="https://https//www.linode.com/austingil" rel="noopener noreferrer"&gt;linode.com/austingil&lt;/a&gt; for some free credits :)&lt;/p&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil" rel="noopener noreferrer"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil" rel="noopener noreferrer"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/user-location-is-a-lie/" rel="noopener noreferrer"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
      <category>cloud</category>
    </item>
    <item>
      <title>I Deployed My Own Cute Lil’ Private Internet (a.k.a. VPC)</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Mon, 18 Mar 2024 16:24:42 +0000</pubDate>
      <link>https://dev.to/austingil/i-deployed-my-own-cute-lil-private-internet-aka-vpc-10ci</link>
      <guid>https://dev.to/austingil/i-deployed-my-own-cute-lil-private-internet-aka-vpc-10ci</guid>
      <description>&lt;p&gt;Recently, I had the pleasure of attending &lt;a href="https://www.developerweek.com/" rel="noopener noreferrer"&gt;DeveloperWeek&lt;/a&gt; in Oakland, CA. In addition to working the Akamai booth, making new friends, and spreading the good word of cloud computing, my team mate, &lt;a href="https://buildwithtalia.com/" rel="noopener noreferrer"&gt;Talia&lt;/a&gt; and I were tasked with creating a demo to showcase the new &lt;a href="https://www.linode.com/docs/products/networking/vpc/" rel="noopener noreferrer"&gt;VPC&lt;/a&gt; product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;A Virtual Private Cloud (VPC) enables private communication between two cloud compute instances, isolating network traffic from other internet users, thus improving security.&lt;/p&gt;

&lt;p&gt;So, how did I decide to showcase this? By building a little Pokémon dashboard, of course.&lt;/p&gt;

&lt;p&gt;I deployed two apps, each consisting of an app server and a database server (four servers total). The first app + database server pair is deployed normally, the second is configured to run within a VPC.&lt;/p&gt;

&lt;p&gt;Each app’s front end is built with &lt;a href="https://qwik.dev/" rel="noopener noreferrer"&gt;Qwik&lt;/a&gt; and uses &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt; for styling. The server-side is powered by &lt;a href="https://qwik.dev/docs/qwikcity/" rel="noopener noreferrer"&gt;Qwik City&lt;/a&gt; (Qwik’s official meta-framework) and runs on &lt;a href="https://nodejs.org/" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; hosted on a &lt;a href="https://www.linode.com/products/shared/" rel="noopener noreferrer"&gt;shared Linode VPS&lt;/a&gt;. The apps also use &lt;a href="https://pm2.io/" rel="noopener noreferrer"&gt;PM2&lt;/a&gt; for process management and &lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt; as a reverse proxy and SSL provisioner. The data is stored in a &lt;a href="https://www.postgresql.org/" rel="noopener noreferrer"&gt;PostgreSQL&lt;/a&gt; database that also runs on a shared Linode VPS. The apps interact with the database using &lt;a href="https://orm.drizzle.team/" rel="noopener noreferrer"&gt;Drizzle&lt;/a&gt;, an Object-Relational Mapper (ORM) for &lt;a href="https://austingil.com/category/javascript/" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt;. The entire infrastructure for both apps is managed with &lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt; using the &lt;a href="https://registry.terraform.io/providers/linode/linode/latest/docs" rel="noopener noreferrer"&gt;Terraform Linode provider&lt;/a&gt;, which was new to me, but made provisioning and destroying infrastructure really fast and easy (once I learned how it all worked).&lt;/p&gt;

&lt;p&gt;If you’re interested, you can find all the code here: &lt;a href="https://github.com/AustinGil/linode-vpc-demo" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/AustinGil/linode-vpc-demo&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;As I mentioned above, the demo deploys two identical apps. There isn’t anything remarkably special about it, but here’s a screenshot.&lt;/p&gt;

&lt;p&gt;(I had to change the Pokémon names for reasons…)&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-98-1080x544.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-98-1080x544.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Screenshot of a dashboard table showing nine Pokemon with their stats. The stats include ID, name, type, hp, attack, defence, sp. attack, sp. defence, and speed."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There’s nothing special about this tech stack. I chose these tools because I like them, not necessarily because they were the &lt;strong&gt;best&lt;/strong&gt; tools for the job.&lt;/p&gt;

&lt;p&gt;The interesting part is the infrastructure.&lt;/p&gt;

&lt;p&gt;When we consider app #1, it’s essentially made up of two servers hosted inside the Akamai cloud, one server for the app and one &lt;strong&gt;server&lt;/strong&gt; for the database. When a user loads the app, the app server pulls the data from the database, constructs the HTML, and returns the result to the user.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fvpc-diagram-1.svg%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fvpc-diagram-1.svg%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Infrastructure diagram showing a user connecting to an app server in the cloud, which connects to a database."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The problem here is how the database connection is configured. In some cases, you may deploy a database server without knowing the IP addresses of the computers you want to allow access from (like the app server). In these cases, it’s not uncommon to allow any computer with the right credentials to connect to the database. This presents a security vulnerability because it could allow for a bad actor to connect to the database and steal sensitive data.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fvpc-diagram-2.svg%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fvpc-diagram-2.svg%3Fquality%3D100%26f%3Dauto%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A bad actor would still need the database host, port, username, and password to get access, so it’s not trivial. And as I said, this is not an uncommon practice, but we can do better.&lt;/p&gt;

&lt;p&gt;If you know the IP address for every computer that needs access, a good solution might be to set up a &lt;a href="https://www.akamai.com/glossary/what-is-a-waf" rel="noopener noreferrer"&gt;firewall&lt;/a&gt; or &lt;a href="https://www.linode.com/docs/products/networking/vlans/" rel="noopener noreferrer"&gt;VLAN&lt;/a&gt;. But if your infrastructure is more dynamic, with servers coming up and down, maintaining lists of IP addresses can be cumbersome. And that’s where VPCs shine. You can configure servers to live within a VPC, and allow communication to flow freely, only between other computers in the network.&lt;/p&gt;

&lt;p&gt;That’s how app #2 is set up. Users can connect to the app server, which allows traffic from the public internet, but also lives within the VPC. The app server connects to the database, which is also in the VPC and only allows connections from within the same network. Then the app server takes the data, builds the HTML, and returns the page to the user.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fvpc-diagram-3.svg%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fvpc-diagram-3.svg%3Fquality%3D100%26f%3Dauto%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a normal user, the experience is identical. The browser loads the table with the modified Pokémon data just fine. The VPC doesn’t change anything for normal users.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-100-1080x548.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-100-1080x548.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Screenshot of a dashboard table showing nine Pokemon with their stats. The stats include ID, name, type, hp, attack, defence, sp. attack, sp. defence, and speed."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For bad actors, however, the experience is different. Even if they somehow manage to get the database access credentials, they would not be able to connect because the network isolation from the VPC. Here, the VPC acts as a virtual firewall, ensuring that only devices with the same network are able to access the database.&lt;/p&gt;

&lt;p&gt;(This concept is sometimes referred to as “segmentation”)&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fvpc-diagram-4.svg%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fvpc-diagram-4.svg%3Fquality%3D100%26f%3Dauto%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Evidence
&lt;/h2&gt;

&lt;p&gt;It’s cool to show a demo and talk about the infrastructure with cute diagrams, but I always want to prove, even if just to myself, that things work as expected. So I thought a good way to test it would be to try connecting directly to both databases using my database client, &lt;a href="https://dbeaver.io/" rel="noopener noreferrer"&gt;DBeaver&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For database #1, I set up a Postgres connection using the host IP address I got from my Akamai dashboard and the port, username, and password I had set up in my Terraform script. The connection worked as expected.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-101.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-101.png%3Fquality%3D100%26f%3Dauto%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For database #2, all I had to change was the IP address, since all the database provisioning was handled by the same script using Terraform. The only difference was that the database server was put inside the same VPC as the app server, and it was configured to only allow connections from any computer within the same network.&lt;/p&gt;

&lt;p&gt;As expected, I got an error when trying to connect, even though I had all the correct information.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-102.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-102.png%3Fquality%3D100%26f%3Dauto%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The error doesn’t mention anything about the VPC. It just says that my IP address is not in the allowed list in the configuration file. This makes sense. I could explicitly add my home’s IP address and gain access to the database if needed, but that’s beside the point.&lt;/p&gt;

&lt;p&gt;The key point is that I did not explicitly add &lt;strong&gt;any&lt;/strong&gt; IP address to the Postgres allow-list. Yet, the app server was able to connect just fine, and everyone else was blocked, thanks to the VPC.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The last thing I’ll touch on is the Terraform code for deploying this application. You can find the whole file here: &lt;a href="https://github.com/AustinGil/linode-vpc-demo/blob/main/terraform/terraform.tf" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/AustinGil/linode-vpc-demo/blob/main/terraform/terraform.tf&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It’s also worth mentioning that I tried to make this Terraform file reusable for other people (or future me). That required a bit more variable and config setup based around the &lt;code&gt;tfvars&lt;/code&gt; file: &lt;a href="https://github.com/AustinGil/linode-vpc-demo/blob/main/terraform/terraform.tfvars.example" rel="noopener noreferrer"&gt;&lt;code&gt;github.com/AustinGil/linode-vpc-demo/blob/main/terraform/terraform.tfvars.example&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Anyway, I’m just going to highlight the key parts.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Configure Terraform Provider
&lt;/h3&gt;

&lt;p&gt;First, since I used the &lt;a href="https://registry.terraform.io/providers/linode/linode/latest/docs" rel="noopener noreferrer"&gt;Linode Terraform provisioner&lt;/a&gt;, it makes sense to know how to set that up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terraform &lt;span class="o"&gt;{&lt;/span&gt;
  required_providers &lt;span class="o"&gt;{&lt;/span&gt;
    linode &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"linode/linode"&lt;/span&gt;
      version &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2.13.0"&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

variable &lt;span class="s2"&gt;"LINODE_TOKEN"&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

provider &lt;span class="s2"&gt;"linode"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  token &lt;span class="o"&gt;=&lt;/span&gt; var.LINODE_TOKEN
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This part sets up the provider as well as a variable which Terraform will either ask you for, or you can provide with the &lt;code&gt;tfvars&lt;/code&gt; file.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Set Up VPC and VPC Subnet
&lt;/h3&gt;

&lt;p&gt;Next, I set up the actual &lt;a href="https://registry.terraform.io/providers/linode/linode/latest/docs/resources/vpc" rel="noopener noreferrer"&gt;&lt;code&gt;vpc&lt;/code&gt; resource&lt;/a&gt; along with a &lt;a href="https://registry.terraform.io/providers/linode/linode/latest/docs/resources/vpc_subnet" rel="noopener noreferrer"&gt;&lt;code&gt;subnet&lt;/code&gt; resource&lt;/a&gt;. This part required a lot of learning on my part.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;resource &lt;span class="s2"&gt;"linode_vpc"&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  label &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-vpc"&lt;/span&gt;
  region &lt;span class="o"&gt;=&lt;/span&gt; var.REGION
&lt;span class="o"&gt;}&lt;/span&gt;
resource &lt;span class="s2"&gt;"linode_vpc_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"vpc_subnet"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  vpc_id &lt;span class="o"&gt;=&lt;/span&gt; linode_vpc.vpc.id
  label &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-vpc-subnet"&lt;/span&gt;
  ipv4 &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.VPC_SUBNET_IP&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Servers can only be added to VPCs in the same region. At the time of writing, there are thirteen regions where VPCs are supported. For the most up-to-date details, refer to the docs: &lt;a href="https://www.linode.com/docs/products/networking/vpc/#availability" rel="noopener noreferrer"&gt;linode.com/docs/products/networking/vpc/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I tried deploying my servers to San Francisco and ran into errors several times before realizing that it wasn’t an available region. So I went with Seattle (&lt;code&gt;"us-sea"&lt;/code&gt;) instead.&lt;/p&gt;

&lt;p&gt;The subnets were also a learning point for me. As a web application developer, I haven’t had to do much networking, so when I was asked to provide “&lt;a href="https://registry.terraform.io/providers/linode/linode/latest/docs/resources/vpc_subnet#ipv4" rel="noopener noreferrer"&gt;The IPv4 range of this subnet in CIDR format&lt;/a&gt;” I had to research.&lt;/p&gt;

&lt;p&gt;Turns out, &lt;a href="https://en.wikipedia.org/wiki/Private_network" rel="noopener noreferrer"&gt;there are three IPv4 address ranges that are reserved for private networks&lt;/a&gt; (such as a VPC):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;10.0.0.0 – 10.255.255.255&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;172.16.0.0 – 172.31.255.255&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;192.168.0.0 – 192.168.255.255&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You have to choose one of these three options, but you have to use &lt;a href="https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing" rel="noopener noreferrer"&gt;CIDR&lt;/a&gt; format, which is a way of representing the IP range you want to use. Don’t ask me for more details, because that’s all I know. Akamai has more &lt;a href="https://www.linode.com/docs/products/networking/vpc/#segment-traffic-into-separate-subnets" rel="noopener noreferrer"&gt;documentation around the subnets&lt;/a&gt;. I just went with &lt;code&gt;10.0.0.0/24&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every server in the &lt;strong&gt;private&lt;/strong&gt; network will have an IPv4 address within that range.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Set Up Application Servers
&lt;/h3&gt;

&lt;p&gt;For Terraform to deploy my application servers, I used the &lt;a href="https://registry.terraform.io/providers/linode/linode/latest/docs/resources/instance" rel="noopener noreferrer"&gt;&lt;code&gt;linode_instance&lt;/code&gt; resource&lt;/a&gt;. I also used the &lt;a href="https://registry.terraform.io/providers/linode/linode/latest/docs/resources/stackscript" rel="noopener noreferrer"&gt;&lt;code&gt;stackscript&lt;/code&gt;&lt;/a&gt; resource to create a reusable deployment script for installing and configuring software. It’s like a &lt;a href="https://www.gnu.org/software/bash/manual/bash.html" rel="noopener noreferrer"&gt;Bash&lt;/a&gt; script that lives in your Akamai cloud dashboard that you can reuse on new servers.&lt;/p&gt;

&lt;p&gt;I won’t include the code here, but it installs Node.js 20 via NVM, installs PM2, clones my project repo, runs the app, and sets up Caddy. You can view the StackScript contents in the source code, but I want to focus on the Terraform stuff.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;resource &lt;span class="s2"&gt;"linode_instance"&lt;/span&gt; &lt;span class="s2"&gt;"application1"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  depends_on &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    linode_instance.database1
  &lt;span class="o"&gt;]&lt;/span&gt;
  image &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"linode/ubuntu20.04"&lt;/span&gt;
  &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"g6-nanode-1"&lt;/span&gt;
  label &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-application1"&lt;/span&gt;
  group &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-group"&lt;/span&gt;
  region &lt;span class="o"&gt;=&lt;/span&gt; var.REGION
  authorized_keys &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; linode_sshkey.ssh_key.ssh_key &lt;span class="o"&gt;]&lt;/span&gt;

  stackscript_id &lt;span class="o"&gt;=&lt;/span&gt; linode_stackscript.configure_app_server.id
  stackscript_data &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"GIT_REPO"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.GIT_REPO,
    &lt;span class="s2"&gt;"START_COMMAND"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.START_COMMAND,
    &lt;span class="s2"&gt;"DOMAIN"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DOMAIN1,
    &lt;span class="s2"&gt;"NODE_PORT"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.NODE_PORT,
    &lt;span class="s2"&gt;"DB_HOST"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; linode_instance.database1.ip_address,
    &lt;span class="s2"&gt;"DB_PORT"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_PORT,
    &lt;span class="s2"&gt;"DB_NAME"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_NAME,
    &lt;span class="s2"&gt;"DB_USER"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_USER,
    &lt;span class="s2"&gt;"DB_PASS"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_PASS,
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
resource &lt;span class="s2"&gt;"linode_instance"&lt;/span&gt; &lt;span class="s2"&gt;"application2"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  depends_on &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;
    linode_instance.database2
  &lt;span class="o"&gt;]&lt;/span&gt;
  image &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"linode/ubuntu20.04"&lt;/span&gt;
  &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"g6-nanode-1"&lt;/span&gt;
  label &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-application2"&lt;/span&gt;
  group &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-group"&lt;/span&gt;
  region &lt;span class="o"&gt;=&lt;/span&gt; var.REGION
  authorized_keys &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; linode_sshkey.ssh_key.ssh_key &lt;span class="o"&gt;]&lt;/span&gt;

  stackscript_id &lt;span class="o"&gt;=&lt;/span&gt; linode_stackscript.configure_app_server.id
  stackscript_data &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"GIT_REPO"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.GIT_REPO,
    &lt;span class="s2"&gt;"START_COMMAND"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.START_COMMAND,
    &lt;span class="s2"&gt;"DOMAIN"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DOMAIN2,
    &lt;span class="s2"&gt;"NODE_PORT"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.NODE_PORT,
    &lt;span class="s2"&gt;"DB_HOST"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_PRIVATE_IP,
    &lt;span class="s2"&gt;"DB_PORT"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_PORT,
    &lt;span class="s2"&gt;"DB_NAME"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_NAME,
    &lt;span class="s2"&gt;"DB_USER"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_USER,
    &lt;span class="s2"&gt;"DB_PASS"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_PASS,
  &lt;span class="o"&gt;}&lt;/span&gt;

  interface &lt;span class="o"&gt;{&lt;/span&gt;
    purpose &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  interface &lt;span class="o"&gt;{&lt;/span&gt;
    purpose   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt;
    subnet_id &lt;span class="o"&gt;=&lt;/span&gt; linode_vpc_subnet.vpc_subnet.id
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuring the two resources is almost identical, with only a few significant things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Application #2 includes configuration to add it to the VPC.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The StackScript needs the IP address for the database. Application #1 uses the public IP address from database #1 (&lt;/strong&gt;&lt;code&gt;linode_instance.database1.ip_address&lt;/code&gt;). Application #2 uses a variable (&lt;code&gt;var.DB_PRIVATE_IP&lt;/code&gt;). This variable will come up later, but it’s the private IP address assigned to database #2, running within the VPC. This can be manually assigned, so I set it to &lt;code&gt;10.0.0.3&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also note that they are deployed to the same region as the VPC, for the reasons I mentioned above.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Set Up Database Servers
&lt;/h3&gt;

&lt;p&gt;The databases are also set up using the &lt;code&gt;linode_instance&lt;/code&gt; and &lt;code&gt;linode_stackscript&lt;/code&gt; resources. Once again, I’ll skip the StackScript contents which you can find in the repo. It installs Postgres, sets up the database and credentials, and provides some configuration options.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;resource &lt;span class="s2"&gt;"linode_instance"&lt;/span&gt; &lt;span class="s2"&gt;"database1"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  image &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"linode/ubuntu20.04"&lt;/span&gt;
  &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"g6-nanode-1"&lt;/span&gt;
  label &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-db1"&lt;/span&gt;
  group &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-group"&lt;/span&gt;
  region &lt;span class="o"&gt;=&lt;/span&gt; var.REGION

  authorized_keys &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; linode_sshkey.ssh_key.ssh_key &lt;span class="o"&gt;]&lt;/span&gt;
  stackscript_id &lt;span class="o"&gt;=&lt;/span&gt; linode_stackscript.configure_db_server.id
  stackscript_data &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"DB_NAME"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_NAME,
    &lt;span class="s2"&gt;"DB_USER"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_USER,
    &lt;span class="s2"&gt;"DB_PASS"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_PASS,
    &lt;span class="s2"&gt;"PG_HBA_ENTRY"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"host all all all md5"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
resource &lt;span class="s2"&gt;"linode_instance"&lt;/span&gt; &lt;span class="s2"&gt;"database2"&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  image &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"linode/ubuntu20.04"&lt;/span&gt;
  &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"g6-nanode-1"&lt;/span&gt;
  label &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-db2"&lt;/span&gt;
  group &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.app_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-group"&lt;/span&gt;
  region &lt;span class="o"&gt;=&lt;/span&gt; var.REGION
  authorized_keys &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; linode_sshkey.ssh_key.ssh_key &lt;span class="o"&gt;]&lt;/span&gt;

  stackscript_id &lt;span class="o"&gt;=&lt;/span&gt; linode_stackscript.configure_db_server.id
  stackscript_data &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"DB_NAME"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_NAME,
    &lt;span class="s2"&gt;"DB_USER"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_USER,
    &lt;span class="s2"&gt;"DB_PASS"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; var.DB_PASS,
    &lt;span class="s2"&gt;"PG_HBA_ENTRY"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"host all all samenet md5"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;

  interface &lt;span class="o"&gt;{&lt;/span&gt;
    purpose &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
  interface &lt;span class="o"&gt;{&lt;/span&gt;
    purpose   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"vpc"&lt;/span&gt;
    subnet_id &lt;span class="o"&gt;=&lt;/span&gt; linode_vpc_subnet.vpc_subnet.id
    ipv4 &lt;span class="o"&gt;{&lt;/span&gt;
      vpc &lt;span class="o"&gt;=&lt;/span&gt; var.DB_PRIVATE_IP
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As with the application servers, the two database servers are mostly the same, with just a couple of key differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The second database includes configuration to add it to the VPC.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Different settings are written to the Client Authentication file (&lt;/strong&gt;&lt;a href="https://www.postgresql.org/docs/current/auth-pg-hba-conf.html" rel="noopener noreferrer"&gt;&lt;code&gt;pg_hba.conf&lt;/code&gt;&lt;/a&gt;). Database #1 allows all internet connections (&lt;code&gt;"host all all all md5"&lt;/code&gt;) while database #2 only allows access from the same network (&lt;code&gt;"host all all samenet md5"&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s also worth noting that we explicitly assign the server’s private IP address when configuring the VPC settings (&lt;code&gt;var.DB_PRIVATE_IP&lt;/code&gt;). This is the same static value that was given to the application server so it can connect to the database from within the VPC.&lt;/p&gt;

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

&lt;p&gt;Hopefully this post has opened our eyes to what VPCs are, why they’re cool, and when you might consider one. It’s like having your own little private internet. It’s not strictly a replacement for VLANs or firewalls, but it’s a great addition to any existing security practice, or at least something to keep in the back of your head.&lt;/p&gt;

&lt;p&gt;Building out the demo was interesting in itself, and there were a lot of things that were totally new to me. I spent a lot of time learning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What VPCs are and how they work.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It was my first time using Terraform, so that involved installation, usage, terminology, etc.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I’ve used Postgres before, but have never had to manually configure client access.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;This was my second project using Drizzle and although it was very limited, the migrations process was challenging.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I learned more than I care to know about networking, computer interfaces, IP ranges, and CIDR. I have much more respect for folks working on the networking layer.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Linode StackScripts are also super cool. It was my preferred way to configure a server using Terraform and I want to see how they work otherwise.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There were also a couple of resources that I found particularly helpful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.linode.com/docs/products/networking/vpc/" rel="noopener noreferrer"&gt;&lt;strong&gt;Linode’s VPC documentation&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://registry.terraform.io/providers/linode/linode/latest/docs" rel="noopener noreferrer"&gt;&lt;strong&gt;Terraform’s Linode provider documentation&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And in case you want to keep going on with this or related topics, Talia put together some excellent posts recently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://buildwithtalia.com/from-code-to-cloud-unpacking-developerweek" rel="noopener noreferrer"&gt;&lt;strong&gt;Talia’s review of DeveloperWeek&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://buildwithtalia.com/introducing-akamais-virtual-private-cloud" rel="noopener noreferrer"&gt;&lt;strong&gt;Introduction to Akamai’s VPC&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://buildwithtalia.com/crafting-a-resilient-vpc-landscape-using-terraform" rel="noopener noreferrer"&gt;&lt;strong&gt;Crafting a Resilient VPC Landscape using Terraform&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://buildwithtalia.com/private-ip-vs-vlan-vs-vpc" rel="noopener noreferrer"&gt;&lt;strong&gt;When should I use a Private IP vs. VLAN vs. VPC?&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And of course, if you are interested in trying out the VPC or any other Akamai cloud computing products, new users can sign up at &lt;a href="https://www.linode.com/austingil" rel="noopener noreferrer"&gt;linode.com/austingil&lt;/a&gt; and get $100 in free credits :)&lt;/p&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil" rel="noopener noreferrer"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil" rel="noopener noreferrer"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/vpc-demo-overview/" rel="noopener noreferrer"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Advanced Architecture for AI Application (AKA AAAA!)</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Fri, 16 Feb 2024 19:25:15 +0000</pubDate>
      <link>https://dev.to/austingil/advanced-architecture-for-ai-application-aka-aaaa-293i</link>
      <guid>https://dev.to/austingil/advanced-architecture-for-ai-application-aka-aaaa-293i</guid>
      <description>&lt;p&gt;Surprise! This is a bonus blog post for the AI for Web Devs series I recently wrapped up. If you haven’t read that series yet, I’d encourage you to &lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;check it out&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This post will look at the existing project architecture and ways we can improve it for both application developers and the end user.&lt;/p&gt;

&lt;p&gt;I’ll be discussing some general concepts, and using specific Akamai products in my examples.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/dvXJmiQY2Xk"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Application Architecture
&lt;/h2&gt;

&lt;p&gt;The existing application is pretty basic. A user submits two opponents, then the application streams back an AI-generated response of who would win in a fight.&lt;/p&gt;

&lt;p&gt;The architecture is also simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The client sends a request to a server.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The server constructs a prompt and forwards the prompt to OpenAI.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenAI returns a streaming response to the server.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The server makes any necessary adjustments and forwards the streaming response to the client.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I used Akamai’s cloud compute services (formerly &lt;a href="https://www.linode.com/lp/try/?ifso=austingil" rel="noopener noreferrer"&gt;Linode&lt;/a&gt;) but this would be the same for any hosting service, really.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fai-architecture-1-1080x556.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fai-architecture-1-1080x556.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Architecture diagram showing a client connecting to a server inside the Cloud, which forwards the request to OpenAI, then returns to the server and back to the client."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🤵 looks like a server at a fancy restaurant, and 👁️‍🗨️ is “a eye”, or AI. lolz&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Technically this works fine, but there are a couple of problems, particularly when users make duplicate requests. It could be faster and more cost-effective to store responses on our server and only go to OpenAI for unique requests.&lt;/p&gt;

&lt;p&gt;This assumes we don’t need every single request to be non-deterministic (the same input produces a different output). Let’s assume it’s OK for the same input to produce the same output. After all, a prediction for who would win in a fight wouldn’t likely change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Database Architecture
&lt;/h2&gt;

&lt;p&gt;If we want to store responses from OpenAI, a practical place to put them is in some sort of database that allows for quick and easy lookup using the two opponents. This way, when a request is made, we can check the database first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The client sends a request to a server.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The server checks for an existing entry in the database that matches the user’s input.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If a previous record exists, the server responds with that data, and the request is complete. Skip the following steps.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If not, the server follows from step three in the previous flow.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Before closing the response, the server stores the OpenAI results in the database.&lt;/strong&gt;&lt;/p&gt;&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fai-architecture-2-1080x747.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fai-architecture-2-1080x747.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Architecture diagram showing a client connecting to a server inside the Cloud, which checks for data in a database, then optionally forwards the request to OpenAI to get the results, then returns the data back to the client."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dotted lines represent optional requests, and the 💽 kind of looks like a hard disk.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With this setup, any duplicate requests will be handled by the database. By making some of the OpenAI requests optional, we can potentially reduce the amount of latency users experience, plus save money by reducing the number of API requests.&lt;/p&gt;

&lt;p&gt;This is a good start, especially if the server and the database exist in the same region. It would make for much quicker response times than going to OpenAI’s servers.&lt;/p&gt;

&lt;p&gt;However, as our application becomes more popular, we may start getting users from all over the world. Faster database lookups are great, but what happens if the bottleneck is the latency from the time spent in flight?&lt;/p&gt;

&lt;p&gt;We can address that concern by moving things closer to the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bring in Edge Compute
&lt;/h2&gt;

&lt;p&gt;If you’re not already familiar with the term “edge”, this part might be confusing, but I’ll try to explain it simply. Edge refers to content being as close to the user as possible. For some people, that could mean IoT devices or cellphone towers, but in the case of the web, the canonical example is a &lt;a href="https://www.akamai.com/glossary/what-is-a-cdn" rel="noopener noreferrer"&gt;Content Delivery Network (CDN)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I’ll spare you the details, but a CDN is a network of globally distributed computers that can respond to user requests from the nearest node in the network (&lt;a href="https://austingil.com/file-uploads-cdn/" rel="noopener noreferrer"&gt;something I’ve written about in the past&lt;/a&gt;). While traditionally they were designed for static assets, in recent years, they started supporting edge compute (&lt;a href="https://austingil.com/edge-compute-knitted-dog-hats/" rel="noopener noreferrer"&gt;also something I’ve written about in the past&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;With edge compute, we can move a lot of our backend logic super close to the user, and it doesn’t stop at compute. Most edge compute providers also offer some sort of eventually-consistent key-value store in the same edge nodes.&lt;/p&gt;

&lt;p&gt;How could that impact our application?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The client sends a request to our backend.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The edge compute network routes the request to the nearest edge node.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The edge node checks for an existing entry in the key-value store that matches the user’s input.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If a previous record exists, the edge node responds with that data and the request is complete. Skip the following steps.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If not, the edge node forwards the request to the origin server, which passes it along to OpenAI and yadda yadda yadda.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Before closing the response, the server stores the OpenAI results in the edge key-value store.&lt;/strong&gt;&lt;/p&gt;&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fai-architecture-3-1080x549.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fai-architecture-3-1080x549.png%3Fquality%3D100%26f%3Dauto%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The edge node is the blue box and represented by 🔪 because it has an edge, EdgeWorker is Akamai’s edge compute product represented by 🧑‍🏭, and EdgeKV is Akamai’s key-value store represented by 🔑🤑🏪. The edge box is closer to the client than the origin server in the cloud to represent physical distance.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The origin server may not be strictly necessary here, but I think it’s more likely to be there. For the sake of data, compute, and logic flow, this is mostly the same as the previous architecture. The main difference being the previously stored results now exist super close to users and can be returned almost immediately.&lt;/p&gt;

&lt;p&gt;(Note: although the data is being cached at the edge, the response is still dynamically constructed. If you don’t need dynamic responses, it may be simpler to use a CDN in front of the origin server and set the correct HTTP headers to cache the response. There’s a lot of nuance here, and I could say more but…well, I’m tired and don’t really want to. Feel free to reach out if you have any questions.)&lt;/p&gt;

&lt;p&gt;Now we’re cooking! Any duplicate requests will be responded to almost immediately, while also saving us unnecessary API requests.&lt;/p&gt;

&lt;p&gt;This sorts out the architecture for the text responses, but we also have AI-generated images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache Those Images
&lt;/h2&gt;

&lt;p&gt;The last thing we’ll consider today is images. When dealing with images, we need to think about delivery and storage. I’m sure that the folks at OpenAI have their own solutions, but some organizations want to own the entire infrastructure for security, compliance, or reliability reasons. Some may even run their own image generation services instead of using OpenAI.&lt;/p&gt;

&lt;p&gt;In the current workflow, the user makes a request that ultimately makes its way to OpenAI. OpenAI generates the image but doesn’t return it. Instead, they return a JSON response with the URL for the image, hosted on OpenAI’s infrastructure. With this response, an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; tag can be added to the page using the URL, which kicks off another request for the actual image.&lt;/p&gt;

&lt;p&gt;If we want to host the image on our own infrastructure, we need a place to store it. We could write the images onto the origin server’s disk, but that could quickly use up the disk space, and we’d have to upgrade our servers, which can be costly. &lt;a href="https://www.linode.com/products/object-storage/" rel="noopener noreferrer"&gt;Object storage&lt;/a&gt; is a much cheaper solution (&lt;a href="https://austingil.com/upload-to-s3/" rel="noopener noreferrer"&gt;I’ve also written about this&lt;/a&gt;). Instead of using the OpenAI URL for the image, we could upload it to our own object storage instance and use that URL instead.&lt;/p&gt;

&lt;p&gt;That solves the storage question, but object storage buckets are generally deployed to a single region. This echoes the problem we had with storing text in a database. A single region may be far away from users, which could cause a lot of latency.&lt;/p&gt;

&lt;p&gt;Having introduced the edge already, it would be pretty trivial to add CDN features for just the static assets (frankly, every site should have a CDN). Once configured, the CDN will pull images from object storage on the initial request and cache them for any future requests from visitors in the same region.&lt;/p&gt;

&lt;p&gt;Here’s how our flow for images would look:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Client sends a request to generate an image based on their opponents&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Edge compute checks if the image data for that request already exists. If so, it returns the URL.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The image is added to the page with the URL and the browser requests the image.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If the image has been previously cached in the CDN, the browser loads it almost immediately. This is the end of the flow.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If the image has not been previously cached, the CDN will pull the image from the object storage location, cache a copy of it for future requests, and return the image to the client. This is another end of the flow.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If the image data is not in the edge key-value store, the request to generate the image goes to the server and on to OpenAI, which generates the image and returns the URL information. The server starts a task to save the image in the object storage bucket, stores the image data in the edge key-value store, and returns the image data to edge compute.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;With the new image data, the client creates the image which creates a new request and continues from step five above.&lt;/strong&gt;&lt;/p&gt;&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fai-architecture-4-1080x697.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fai-architecture-4-1080x697.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Architecture diagram showing a client connecting to an edge node which checks the edge key-value store, then optionally passes the request to a cloud server and on to OpenAI before returning the data to the client. Additionally, if the user makes a request for an image, the request will check a CDN first, and if it doesn't exist, will pull it from Object Storage where it was placed from OpenAI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content delivery network denoted by delivery truck (🚚) and network signal (📶), and object storage denoted by socks in a box (🧦📦), or objects in storage. This caption is probably not necessary, as I think these are clear, but I’m too proud of my emoji game and require validation. Thank you for indulging me. Carry on.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This last architecture is, admittedly, a little bit more complex, but if your application is going to handle serious traffic, it’s worth considering.&lt;/p&gt;

&lt;h2&gt;
  
  
  Voilà
&lt;/h2&gt;

&lt;p&gt;Right on! With all those changes in place, we have created AI-generated text and images for unique requests and serve cached content from the edge for duplicate requests. The result is faster response times and a much better user experience (in addition to fewer API calls).&lt;/p&gt;

&lt;p&gt;I kept these architecture diagrams applicable across various databases, edge compute, object storage, and CDN providers on purpose. I like my content to be broadly applicable. But it’s worth mentioning that integrating the edge is about more than just performance. There are a lot of really cool security features you can enable as well.&lt;/p&gt;

&lt;p&gt;For example, on Akamai’s network, you can have access to things like &lt;a href="https://www.akamai.com/products/app-and-api-protector" rel="noopener noreferrer"&gt;web application firewall (WAF)&lt;/a&gt;, &lt;a href="https://www.akamai.com/products/prolexic-solutions" rel="noopener noreferrer"&gt;distributed denial of service (DDoS) protection&lt;/a&gt;, &lt;a href="https://www.akamai.com/products/bot-manager" rel="noopener noreferrer"&gt;intelligent bot detection&lt;/a&gt;, and more. That’s all beyond the scope of today’s post, though.&lt;/p&gt;

&lt;p&gt;So for now, I’ll leave you with a big “thank you” for reading. I hope you learned something. And as always, feel free to reach out any time with comments, questions, or concerns.&lt;/p&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil" rel="noopener noreferrer"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil" rel="noopener noreferrer"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/advanced-architecture-for-ai-application-aka-aaaa/" rel="noopener noreferrer"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AI for Web Devs: Deploying Your AI App to Production</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Wed, 07 Feb 2024 17:28:40 +0000</pubDate>
      <link>https://dev.to/austingil/ai-for-web-devs-deploying-your-ai-app-to-production-5beb</link>
      <guid>https://dev.to/austingil/ai-for-web-devs-deploying-your-ai-app-to-production-5beb</guid>
      <description>&lt;p&gt;Welcome back to the series where we have been building an application with &lt;a href="https://qwik.builder.io/" rel="noopener noreferrer"&gt;Qwik&lt;/a&gt; that incorporates &lt;a href="https://austingil.com/category/ai/" rel="noopener noreferrer"&gt;AI&lt;/a&gt; tooling from &lt;a href="https://openai.com/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;. So far we’ve created a pretty cool app that uses AI to generate text and images.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/" rel="noopener noreferrer"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/" rel="noopener noreferrer"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/" rel="noopener noreferrer"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/" rel="noopener noreferrer"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/" rel="noopener noreferrer"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now, there’s just one more thing to do. It’s launch time!&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/pob822xaT4Y"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I’ll be deploying to &lt;a href="https://www.akamai.com/" rel="noopener noreferrer"&gt;Akamai&lt;/a&gt;‘s cloud computing services (formerly &lt;a href="https://www.linode.com/lp/try/?ifso=austingil" rel="noopener noreferrer"&gt;Linode&lt;/a&gt;), but these steps should work with any VPS provider. If you don’t already have a hosting provider, you can sign up at &lt;a href="https://www.linode.com/austingil" rel="noopener noreferrer"&gt;linode.com/austingil&lt;/a&gt; to get $100 in cloud computing credits.&lt;/p&gt;

&lt;p&gt;Let’s do this!&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup Runtime Adapter
&lt;/h2&gt;

&lt;p&gt;There are a couple of things we need to get out of the way first: deciding where we are going to run our app, what runtime it will run in, and how the deployment pipeline should look.&lt;/p&gt;

&lt;p&gt;As I mentioned before, I’ll be deploying to a VPS in Akamai’s connected cloud, but any other VPS should work. For the runtime, I’ll be using Node.js, and I’ll keep the deployment simple by using Git.&lt;/p&gt;

&lt;p&gt;Qwik is cool because it’s designed to run in multiple JavaScript runtimes. That’s handy, but it also means that our code isn’t ready to run in production as is. Qwik needs to be aware of its runtime environment, which we can do with adapters.&lt;/p&gt;

&lt;p&gt;We can access see and install available adapters with the command, &lt;code&gt;npm run qwik add&lt;/code&gt;. This will prompt us with several options for adapters, integrations, and plugins.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-97.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-97.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="The resulting screen from  raw `npm run qwik add` endraw  command, showing the list of integrations."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For my case, I’ll go down and select the &lt;a href="https://fastify.dev/" rel="noopener noreferrer"&gt;Fastify&lt;/a&gt; adapter. It works well on a VPS running Node.js. You can select a different target if you prefer.&lt;/p&gt;

&lt;p&gt;Once you select your integration, the terminal will show you the changes it’s about to make and prompt you to confirm. You’ll see that it wants to modify some files, create some new ones, install dependencies, and add some new NPM scripts. Make sure you’re comfortable with these changes before confirming.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-99.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-99.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Qwik CLI screen showing all the proposed changes and asking for approval"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once these changes are installed, your app will have what it needs to run in production. You can test this by building the production assets and running the &lt;code&gt;serve&lt;/code&gt; command. (Note: For some reason, &lt;code&gt;npm run build&lt;/code&gt; always hangs for me, so I run the client and server build scripts separately).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;npm run build.client &amp;amp; npm run build.server &amp;amp; npm run serve&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This will build out our production assets and start the production server listening for requests at &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;. If all goes well, you should be able to open that URL in your browser and see your app there. It won’t actually work because it’s missing the OpenAI API keys, but we’ll sort that part out on the production server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Push Changes to Git Repo
&lt;/h2&gt;

&lt;p&gt;As mentioned above, this deployment process is going to be focused on simplicity, not automation. So rather than introducing more complex tooling like Docker containers or Kubernetes, we’ll stick to a simpler, but more manual process: using Git to deploy our code.&lt;/p&gt;

&lt;p&gt;I’ll assume you already have some familiarity with Git, and a remote repo you can push to. If not, please go make one now.&lt;/p&gt;

&lt;p&gt;You’ll need to commit your changes and push it to your repo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-am&lt;/span&gt; &lt;span class="s2"&gt;"ready to commit 💍"&lt;/span&gt; &amp;amp; git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Prepare Production Server
&lt;/h2&gt;

&lt;p&gt;If you already have a VPS ready, feel free to skip this section. I’ll be deploying to an Akamai VPS. If you don’t already have an account, feel free to sign up at &lt;a href="https://www.linode.com/austingil" rel="noopener noreferrer"&gt;linode.com/austingil&lt;/a&gt; for $100 in free credits.&lt;/p&gt;

&lt;p&gt;I won’t walk through the step-by-step process for setting up a server, but in case you’re interested, I chose the Nanode 1 GB shared CPU plan for $5/month with the following specs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Operating system: Ubuntu 22.04 LTS&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Location: Seattle, WA&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CPU: 1&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;RAM: 1 GB&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Storage: 25 GB&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transfer: 1 TB&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choosing different specs shouldn’t make a difference when it comes to running your app, although some of the commands to install any dependencies may be different. If you’ve never done this before, then try to match what I have above. You can even use a different provider, as long as you’re deploying to a server to which you have SSH access.&lt;/p&gt;

&lt;p&gt;Once you have your server provisioned and running, you should have a public IP address that looks something like &lt;code&gt;172.100.100.200&lt;/code&gt;. You can log into the server from your terminal with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh root@172.100.100.200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll have to provide the root password if you have not already set up an authorized key.&lt;/p&gt;

&lt;p&gt;We’ll use Git as a convenient tool to get our code from our repo into our server, so that will need to be installed. But before we do that, I always recommend updating the existing software. We can do the update and installation with the following command.&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="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;git &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our server also needs Node.js to run our app. We could install the binary directly, but I prefer to use a tool called &lt;a href="https://github.com/nvm-sh/nvm" rel="noopener noreferrer"&gt;NVM&lt;/a&gt;, which allows us to easily manage Node versions. We can install it with this command:&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;-o-&lt;/span&gt; https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And once NVM is installed, you can install the latest version of Node with:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Note that the terminal may say that NVM is not installed. If you exit the server and sign back in, it should work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upload, Build, &amp;amp; Run App
&lt;/h2&gt;

&lt;p&gt;With our server set up, it’s time to get our code installed. With Git, it’s relatively easy. We can copy our code into our server using the &lt;code&gt;clone&lt;/code&gt; command. You’ll want to use your own repo, but it should look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/AustinGil/versus.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our source code is now on the server, but it’s still not quite ready to run. We still need to install the NPM dependencies, build the production assets, and provide any environment variables.&lt;/p&gt;

&lt;p&gt;Let’s do!&lt;/p&gt;

&lt;p&gt;First, navigate to the folder where you just cloned the project. I used:&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="nb"&gt;cd &lt;/span&gt;versus
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The install is easy enough:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The build command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However, if you have any type-checking or linting errors, it will hang there. You can either fix the errors (which you probably should) or bypass them and build anyway with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build.client &amp;amp; npm run build.server
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/austingil/versus" rel="noopener noreferrer"&gt;The latest version of the project&lt;/a&gt; source code has working types if you want to check that.&lt;/p&gt;

&lt;p&gt;The last step is a bit tricky. As we saw above, environment variables will not be injected from the &lt;code&gt;.env&lt;/code&gt; file when running the production app. Instead, we can provide them at runtime right before the &lt;code&gt;serve&lt;/code&gt; command like this:&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="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_api_key npm run serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll want to provide your own API key there in order for the OpenAI requests to work.&lt;/p&gt;

&lt;p&gt;Also, for Node.js deployments, there’s an extra, necessary step. You must also set an &lt;code&gt;ORIGIN&lt;/code&gt; variable assigned to the full URL where the app will be running. Qwik needs this information to properly configure their &lt;a href="https://owasp.org/www-community/attacks/csrf" rel="noopener noreferrer"&gt;CSRF&lt;/a&gt; protection.&lt;/p&gt;

&lt;p&gt;If you don’t know the URL, you can disable this feature in the &lt;code&gt;/src/entry.preview.tsx&lt;/code&gt; file by setting the &lt;code&gt;createQwikCity&lt;/code&gt; options property &lt;code&gt;checkOrigin&lt;/code&gt; to &lt;code&gt;false&lt;/code&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="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;createQwikCity&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;qwikCityPlan&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;checkOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This process is &lt;a href="https://qwik.builder.io/docs/deployments/node/" rel="noopener noreferrer"&gt;outlined in more detail in the docs&lt;/a&gt;, but it’s recommended not to disable, as CSRF can be quite dangerous. And anyway, you’ll need a URL to deploy the app anyway, so better to just set the &lt;code&gt;ORIGIN&lt;/code&gt; environment variable. Note that if you make this change, you’ll want to redeploy and rerun the build and serve commands.&lt;/p&gt;

&lt;p&gt;If everything is configured correctly and running, you should start seeing the logs from Fastify in the terminal, confirming that the app is up and running.&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;level&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;time&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1703810454465&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;23834&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&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;localhost&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;msg&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;Server listening at http://[::1]:3000&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, accessing the app via IP address and port number doesn’t show the app (at least not for me). This is likely a networking issue, but also something that will be solved in the next section, where we run our app at the root domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Missing Steps
&lt;/h2&gt;

&lt;p&gt;Technically, the app is deployed, built, and running, but in my opinion, there is a lot to be desired before we can call it “production ready.” Some tutorials would assume you know how to do the rest, but I don’t want to do you like that. We’re going to cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Running the app in background mode&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Restarting the app if the server crashes&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Accessing the app at the root domain&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Setting up an SSL certificate&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One thing you will need to do for yourself is buy the domain name. There are lots of good places. I’ve been a fan of &lt;a href="https://porkbun.com/" rel="noopener noreferrer"&gt;Porkbun&lt;/a&gt; and &lt;a href="https://www.namesilo.com/" rel="noopener noreferrer"&gt;Namesilo&lt;/a&gt;. I don’t think there’s a huge difference for which registrar you use, but I like these because they offer WHOIS privacy and email forwarding at no extra charge on top of their already low prices.&lt;/p&gt;

&lt;p&gt;Before we do anything else on the server, it’ll be a good idea to point your domain name’s A record (&lt;code&gt;@&lt;/code&gt;) to the server’s IP address. Doing this sooner can help with propagation times.&lt;/p&gt;

&lt;p&gt;Now, back in the server, there’s one glaring issue we need to deal with first. When we run the &lt;code&gt;npm run serve&lt;/code&gt; command, our app will run as long as we keep the terminal open. Obviously, it would be nice to exit out of the server, close our terminal, and walk away from our computer to go eat pizza without the app crashing. So we’ll want to run that command in the background.&lt;/p&gt;

&lt;p&gt;There are plenty of ways to accomplish this: Docker, Kubernetes, Pulumis, etc., but I don’t like to add too much complexity. So for a basic app, I like to use &lt;a href="https://www.npmjs.com/package/pm2" rel="noopener noreferrer"&gt;PM2&lt;/a&gt;, a Node.js process manager with great features, including the ability to run our app in the background.&lt;/p&gt;

&lt;p&gt;From inside your server, run this command to install PM2 as a global NPM module:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Once it’s installed, we can tell PM2 what command to run with the “&lt;code&gt;start&lt;/code&gt;” command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 start &lt;span class="s2"&gt;"npm run serve"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PM2 has a lot of really nice features in addition to running our apps in the background. One thing you’ll want to be aware of is the command to view logs from your app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In addition to running our app in the background, PM2 can also be configured to start or restart any process if the server crashes. This is super helpful to avoid downtime. You can set that up with this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 startup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ok, our app is now running, and will continue to run after a server restart. Great!&lt;/p&gt;

&lt;p&gt;But we still can’t get to it. Lol!&lt;/p&gt;

&lt;p&gt;My preferred solution is using &lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt;. This will resolve the networking issues, work as a great reverse proxy, and takes care of the whole SSL process for us. We can follow the &lt;a href="https://caddyserver.com/docs/install#debian-ubuntu-raspbian" rel="noopener noreferrer"&gt;install instructions from their documentation&lt;/a&gt; and run these five commands:&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="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; debian-keyring debian-archive-keyring apt-transport-https curl
curl &lt;span class="nt"&gt;-1sLf&lt;/span&gt; &lt;span class="s1"&gt;'https://dl.cloudsmith.io/public/caddy/stable/gpg.key'&lt;/span&gt; | &lt;span class="nb"&gt;sudo &lt;/span&gt;gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl &lt;span class="nt"&gt;-1sLf&lt;/span&gt; &lt;span class="s1"&gt;'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt'&lt;/span&gt; | &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/apt/sources.list.d/caddy-stable.list
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;caddy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that’s done, you can go to your server’s IP address and you should see the default Caddy welcome page:&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2FScreenshot-from-2024-01-02-12-42-04-1080x813.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2FScreenshot-from-2024-01-02-12-42-04-1080x813.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Default Caddy homepage with instructions on how to get started."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Progress!&lt;/p&gt;

&lt;p&gt;In addition to showing us something is working, this page also gives us some handy information on how to work with Caddy.&lt;/p&gt;

&lt;p&gt;Ideally, you’ve already pointed your domain name to the server’s IP address. Next, we’ll want to modify the Caddyfile:&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="nb"&gt;sudo &lt;/span&gt;nano /etc/caddy/Caddyfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As their instructions suggest, we’ll want to replace the &lt;code&gt;:80&lt;/code&gt; line with our domain (or subdomain), but instead of uploading static files or changing the site root, I want to remove (or comment out) the &lt;code&gt;root&lt;/code&gt; line and enable the &lt;code&gt;reverse_proxy&lt;/code&gt; line, pointing the reverse proxy to my Node.js app running at port 3000.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;versus.austingil.com {
        reverse_proxy localhost:3000
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After saving the file and reloading Caddy (&lt;code&gt;systemctl reload caddy&lt;/code&gt;), the new Caddyfile changes should take effect. Note that it may take a few moments before the app is fully up and running. This is because one of Caddy’s features is to provision a new SSL certificate for the domain. It also sets up the automatic redirect from HTTP to HTTPS.&lt;/p&gt;

&lt;p&gt;So now if you go to your domain (or subdomain), you should be redirected to the HTTPS version running a reverse-proxy in front of your generative AI application which is resilient to server crashes.&lt;/p&gt;

&lt;p&gt;How awesome is that!?&lt;/p&gt;

&lt;p&gt;Using PM2 we can also enable some load-balancing in case you’re running a server with multiple cores. The full PM2 command including environment variables and load-balancing might look something like this:&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="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_api_key &lt;span class="nv"&gt;ORIGIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;example.com pm2 start &lt;span class="s2"&gt;"npm run serve"&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that you may need to remove the current instance from PM2 and rerun the start command, you don’t have to restart the Caddy process unless you change the Caddy file, and any changes to the Node.js source code will require a rebuild before running it again.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hell yeah! We did it!
&lt;/h2&gt;

&lt;p&gt;Alright, that’s it for this blog post, &lt;strong&gt;and&lt;/strong&gt; this series. I sincerely hope you enjoyed both and learned some cool things. Today, we covered a lot of things you need to know to deploy an AI-powered application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Runtime adapters&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Building for production&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Environment variables&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Process managers&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reverse-proxies&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SSL certificates&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you missed any of the previous posts, be sure to go back and check them out.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/" rel="noopener noreferrer"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/" rel="noopener noreferrer"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/" rel="noopener noreferrer"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/" rel="noopener noreferrer"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/" rel="noopener noreferrer"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I’d love to know what you thought about the whole series. If you want, you can play with the app I built at &lt;a href="https://versus.austingil.com/" rel="noopener noreferrer"&gt;versus.austingil.com&lt;/a&gt;. Let me know if you deployed your own app, and I’ll link to it from here. Also, if you have ideas for topics you’d like me to discuss in the future I’d love to hear them :)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UPDATE:&lt;/strong&gt; If you liked this project and are curious to see what it might look like as a &lt;a href="https://kit.svelte.dev/" rel="noopener noreferrer"&gt;SvelteKit&lt;/a&gt; app, &lt;a href="https://www.timsmith.tech/blog/converting-a-qwik-ai-app-to-sveltekit" rel="noopener noreferrer"&gt;check out this blog post by Tim Smith&lt;/a&gt; where he converts this existing app over.&lt;/p&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil" rel="noopener noreferrer"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil" rel="noopener noreferrer"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/ai-for-web-devs-deploying/" rel="noopener noreferrer"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI for Web Devs: Addressing Bugs, Security, &amp; Reliability</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Wed, 31 Jan 2024 17:58:48 +0000</pubDate>
      <link>https://dev.to/austingil/ai-for-web-devs-addressing-bugs-security-reliability-4jnm</link>
      <guid>https://dev.to/austingil/ai-for-web-devs-addressing-bugs-security-reliability-4jnm</guid>
      <description>&lt;p&gt;Welcome back to this series where we have been learning how to build web applications with &lt;a href="https://austingil.com/category/ai/"&gt;AI&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So far in this series, we’ve created a working app that uses AI to determine who would win in a fight between two user-provided opponents and generates text responses and images. It’s working, but we’ve been following the happy path.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this post, we’re going to talk about what happens when things don’t follow the happy path by accounting for error handling and security concerns.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/DpOTSKtpEHI"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealing With Bad HTTP Requests
&lt;/h2&gt;

&lt;p&gt;The first issue to deal with is around our &lt;a href="https://httpwg.org/specs/"&gt;HTTP&lt;/a&gt; requests. So far, we’ve just been assuming that the requests from the client to the server will just work.&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Do something with response&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a mistake. We need to account for situations where the server experiences an error or returns a bad status code.&lt;/p&gt;

&lt;p&gt;In a real-world application, we would want a sophisticated notification service to communicate to users in the event of different errors (server error, validation error, authorization error, not found error, etc.). It would also be good to tie in an error and bug-tracking software so you get notified of any issues.&lt;/p&gt;

&lt;p&gt;For today’s example, the discount brand will have to do. We’ll check the response &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Response/ok"&gt;&lt;code&gt;ok&lt;/code&gt;&lt;/a&gt; property and in case of a bad response, we’ll just &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/alert"&gt;&lt;code&gt;alert&lt;/code&gt;&lt;/a&gt; the user that there was an error.&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&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="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;The request experienced an issue.&lt;/span&gt;&lt;span class="dl"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code above only accounts for the HTTP request between the client and the server. Don’t forget, we have another request between the server and OpenAI.&lt;/p&gt;

&lt;p&gt;Consider the scenario where OpenAI returns a bad status code. How should we communicate that to the end user on the client? This is also tricky and unique to each app. For the sake of convenience, we can do a similar check on the &lt;code&gt;response.ok&lt;/code&gt; property. In the event of a bad request, you’ll once again want to report on the error, and maybe respond to the user with the same status code. I would recommend against passing the response message to the client in case it contains sensitive data.&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="c1"&gt;// ... fetch options&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;reportError&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="k"&gt;throw&lt;/span&gt; &lt;span class="nf"&gt;error&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="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ERROR: Service unavailable&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;This error handling is very rudimentary, and I’ll leave it like that because unfortunately, I’ve never seen two apps that handle errors the same way. It’s highly subjective. Suffice it to say that you should spend time thinking about how &lt;strong&gt;your&lt;/strong&gt; app should behave in the event of an error. How do you report it internally, and how do you communicate it to users?&lt;/p&gt;

&lt;p&gt;And what happens when users deliberately try to break something…?&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealing With Bad User Input
&lt;/h2&gt;

&lt;p&gt;In addition to following the happy path where we assumed every HTTP request would always work, we assumed every user was benevolent. This is another mistake. Sometimes users are malicious. Oftentimes, they are just plain silly. We should account for both.&lt;/p&gt;

&lt;p&gt;Any time you receive user-submitted data, you have to validate it. In our app, we expect the user to submit two opponents. What happens if they submit just one, or none, or empty strings? We probably should catch that early before sending the malstructured prompt to OpenAI.&lt;/p&gt;

&lt;p&gt;We can add the HTML &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/required"&gt;&lt;code&gt;required&lt;/code&gt;&lt;/a&gt; attribute to the textareas to tell the form that both inputs need to be filled before the form can be submitted. If the user tries to submit the form without the controls filled in, the browser will prevent the submission, focus on the first invalid input, and provide a little error message telling the user what the problem is.&lt;/p&gt;

&lt;p&gt;This is good for the user experience because it provides some early feedback, but client-side validation is easily bypassed, so we have to also validate data on the server. Fortunately, there are some very good validation libraries available that can help with this. My favorite is called &lt;a href="https://www.npmjs.com/package/zod"&gt;Zod&lt;/a&gt;. We can install it with &lt;code&gt;npm install zod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Zod allows us to define a schema that will be used to validate input data. If the input doesn’t match the schema, Zod can either throw an error or report it.&lt;/p&gt;

&lt;p&gt;In our app, we are receiving the user input through the &lt;code&gt;requestEvent.parseBody()&lt;/code&gt; method, which returns the submitted form data as an object containing &lt;code&gt;opponent1&lt;/code&gt; and &lt;code&gt;opponent2&lt;/code&gt; properties. So, what we need to do is create a validation schema, then pass the form data into one of the schema validation methods.&lt;/p&gt;

&lt;p&gt;I prefer &lt;strong&gt;not&lt;/strong&gt; throwing an error, and instead getting an object back with the validation information. That way, I can add the logic myself to deal with bad data.&lt;/p&gt;

&lt;p&gt;Inside my &lt;code&gt;onPost&lt;/code&gt; &lt;a href="https://qwik.builder.io/docs/middleware/"&gt;middleware&lt;/a&gt;, before doing too much work, let’s make sure we have the right data:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&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;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequestHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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;formData&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;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseBody&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;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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;validation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&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;validation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validation&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="nx"&gt;issues&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;// Continue with OpenAI API request and response&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the code above, I create an Object schema that should have two properties, &lt;code&gt;opponent1&lt;/code&gt; and &lt;code&gt;opponent2&lt;/code&gt;. Both properties are required, must be strings, and cannot be empty. Passing the form data into the schema’s &lt;a href="https://www.npmjs.com/package/zod#safeparse"&gt;&lt;code&gt;safeParse()&lt;/code&gt;&lt;/a&gt; method will return an object that can tell me if the validation was successful, what the error was, if any, and the validated data.&lt;/p&gt;

&lt;p&gt;In the event of invalid data, I return early from the request handler with an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400"&gt;HTTP 400&lt;/a&gt; error response explaining the errors. 400 is the Bad Request status code.&lt;/p&gt;

&lt;p&gt;One other thing I like to change is how I use the form data once it’s been validated. Zod also provides a &lt;code&gt;data&lt;/code&gt; property on the returned object from &lt;code&gt;safeParse&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;promptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validation&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;opponent1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;validation&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;opponent2&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our example, it doesn’t make too much of a difference whether we use this or the form data directly, but it’s nice to get in the habit of using the data property because Zod will coerce the data to the appropriate format. Form data and query parameters are almost always received as strings, but if your Zod schema was expecting a number, it will try to coerce it for you, turning something like the string &lt;code&gt;"420"&lt;/code&gt; into the number &lt;code&gt;420&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So that covers users sending missing data or not enough data, but what about sending too much?&lt;/p&gt;

&lt;p&gt;By giving users unbounded input length that gets injected directly into our prompt, we are opening the gates for users to create massive prompts that would require a lot of tokens and cost us money. Why don’t we add a maximum length to the inputs to something more appropriate for this app?&lt;/p&gt;

&lt;p&gt;We can add a maximum length to both the server-side validation schema and the client-side validation attributes.&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="c1"&gt;// Reusable constant&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;MAX_INPUT_LENGTH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;

&lt;span class="c1"&gt;// In our schema&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX_INPUT_LENGTH&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;MAX_INPUT_LENGTH&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// In our template&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt;
  &lt;span class="nx"&gt;maxLength&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;MAX_INPUT_LENGTH&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By reducing the amount of data a user can provide, we are reducing the amount of tokens that our API request could potentially use.&lt;/p&gt;

&lt;p&gt;This step also limits the amount of flexibility a user has to manipulate our prompt. Consider the fact that a user could provide an “opponent” that actually contains malicious instructions for the app.&lt;/p&gt;

&lt;p&gt;This segues nicely to a very important security concern for AI applications specifically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealing With Injection Attacks
&lt;/h2&gt;

&lt;p&gt;We’re doing some basic validation that the data we get from the user is the right type, but we aren’t checking the content that they send us. We’re just blindly sticking it into our prompt and sending it off, and this opens us up to a very interesting kind of attack called a prompt injection attack.&lt;/p&gt;

&lt;p&gt;If you’ve done any work building applications with SQL, this may sound similar to an &lt;a href="https://owasp.org/www-community/attacks/SQL_Injection"&gt;SQL injection attack&lt;/a&gt;, and that’s because it is. An SQL injection attack is when a user submits some data of the right type, but containing SQL commands that could be harmful if run.&lt;/p&gt;

&lt;p&gt;Here’s an example. Let’s say our app had some SQL logic to select a user by ID based on the input provided:&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT * FROM Users WHERE UserId = &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;inputId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker could provide the string &lt;code&gt;'1 OR 1=1'&lt;/code&gt; as the input and would return the information of all the users. This is bad, but you can avoid it by using parameterized queries, stored procedures, or escaping user input. Unless you’re writing raw SQL queries, most tools protect against injection. If you’re interested, here’s &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html"&gt;more on prevention from OWASP&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Prompt injection is a bit different because prompts don’t have a structured language with specific keywords you can search for. Literally anything you (or the user) provide is a valid prompt, and there’s no easy delineation between what you write and what a user writes.&lt;/p&gt;

&lt;p&gt;To their credit, OpenAI does include tactics for prompt engineering that &lt;a href="https://platform.openai.com/docs/guides/prompt-engineering/strategy-write-clear-instructions"&gt;encourage using delimiters to indicate separate parts of the input&lt;/a&gt;. This way, you can help the AI identify the different identities, like the system and the user.&lt;/p&gt;

&lt;p&gt;It might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Translate&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;delimiting&lt;/span&gt; &lt;span class="nx"&gt;characters&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;text&lt;/span&gt; &lt;span class="nx"&gt;to&lt;/span&gt; &lt;span class="nx"&gt;be&lt;/span&gt; &lt;span class="nx"&gt;translated&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a big improvement as it provides a clearer separation between the system and the user input. Still, it’s not without gaps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://simonwillison.net/"&gt;Simon Willison&lt;/a&gt; has pointed out several examples of prompt injection attacks and why it may never be a solved problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://simonwillison.net/2022/Sep/12/prompt-injection/"&gt;&lt;strong&gt;Prompt injection attacks against GPT-3&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://simonwillison.net/2022/Sep/16/prompt-injection-solutions/"&gt;&lt;strong&gt;I don’t know how to solve prompt injection&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s kind of scary and should make you think twice about building AI-powered apps. But this is a bit of a &lt;a href="https://en.wikipedia.org/wiki/Pandora%27s_box"&gt;Pandora’s box&lt;/a&gt;. Even with its inherent vulnerabilities, AI is here to stay. My suggestion is to keep yourself up to date on attack vectors and incorporate several layers of security.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building applications with the happy path in mind is great, but we must also address the not-so-happy path. It’s important to familiarize ourselves with points of failure and vulnerabilities and address them appropriately. This makes our applications more secure for our users and more reliable. &lt;/p&gt;

&lt;p&gt;In this post, we discussed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What happens when our app experiences HTTP errors?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;How do we validate user input?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;What are some security concerns specifically for AI apps?&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a comprehensive list, but I hope it serves as a good starting point. With this work out of the way, I think we are ready to launch our app to the world. We’ll do that in the next post.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI for Web Devs: AI Image Generation</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Mon, 29 Jan 2024 21:48:18 +0000</pubDate>
      <link>https://dev.to/austingil/ai-for-web-devs-ai-image-generation-3bd5</link>
      <guid>https://dev.to/austingil/ai-for-web-devs-ai-image-generation-3bd5</guid>
      <description>&lt;p&gt;Welcome back to this series where we are learning how to integrate &lt;a href="https://austingil.com/category/ai/"&gt;AI&lt;/a&gt; products into web application.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this post, we are going to use AI to generate images. Before we get to that, let’s add a dialog component to our app. This is a modal-like pop-up designed to show content and be dismissed with the mouse or keyboard.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/V3DjC6-sGBA"&gt;
&lt;/iframe&gt;
 &lt;/p&gt;
&lt;h2&gt;
  
  
  Create A Dialog Component
&lt;/h2&gt;

&lt;p&gt;Before showing an image, we need a place to put it. I think it would be nice to have a dialog that pops up to showcase the image. This is a good opportunity to spend more time with Qwik components.&lt;/p&gt;

&lt;p&gt;By convention, our new component should go in the &lt;code&gt;./src/components&lt;/code&gt; folder. I’ll call mine &lt;code&gt;Dialog.jsx&lt;/code&gt; (or &lt;code&gt;.tsx&lt;/code&gt; if you prefer TypeScript). A Qwik component file should have a default export of a Qwick component. To create one, we use the &lt;a href="https://qwik.builder.io/docs/components/overview/#component"&gt;&lt;code&gt;component$&lt;/code&gt;&lt;/a&gt; function from &lt;code&gt;@builder.io/qwik&lt;/code&gt;. This function takes a function component that returns JSX:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;props&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt; &lt;span class="nx"&gt;markup&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;At the moment, it’s not very useful. Let’s plan out the API design for this dialog. It should have the following characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Opened by clicking on a button.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Can be opened programmatically from the parent.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Closed by pressing Esc key.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Closed by clicking the dialog background.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Can be closed programmatically from the parent.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://austingil.com/category/html/"&gt;HTML&lt;/a&gt; has a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog"&gt;&lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;&lt;/a&gt; element which is great, but it doesn’t quite offer the API that I’m looking for. With a little bit of work, we can fill in the gaps.&lt;/p&gt;

&lt;p&gt;Let’s start with a component that provides a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button"&gt;&lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&gt;&lt;/a&gt; element for controlling the dialog and the &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt; element that will contain any content you put inside. Clicking the button should trigger the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/showModal"&gt;&lt;code&gt;dialog.showModal()&lt;/code&gt;&lt;/a&gt; method. Clicking outside the dialog should trigger the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close"&gt;&lt;code&gt;dialog.close()&lt;/code&gt;&lt;/a&gt; method. Pressing the Esc key closes the dialog already, so that’s sorted. To trigger the dialog methods, we need access to the DOM node, which we can get by &lt;a href="https://qwik.builder.io/docs/components/overview/#getting-hold-of-dom-element"&gt;using a ref and a signal&lt;/a&gt;. In our dialog component, we can add content using a &lt;a href="https://qwik.builder.io/docs/components/slots/#slots"&gt;&lt;code&gt;&amp;lt;Slot&amp;gt;&lt;/code&gt;&lt;/a&gt;, a flexible space inside the dialog where you can put content. We should also provide a btnText prop, to allow for customization of the text on the button that opens the dialogue.&lt;/p&gt;

&lt;p&gt;Here’s what I have so far:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSignal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Slot&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;btnText&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;dialogRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSignal&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;dialogRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showModal&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="nx"&gt;btnText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;
        &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;dialogRef&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;onClick$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dialog&lt;/span&gt;&lt;span class="dl"&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;dialogRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Slot&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/dialog&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="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;Note the additional &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div"&gt;&lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;&lt;/a&gt; inside the &lt;code&gt;&amp;lt;dialog&amp;gt;&lt;/code&gt;. This lets me add some padding, but more importantly, it helps me track whether a click on the dialog happened on the background (the dialog element) or on the content (the div and children). This lets us close the dialog only when the background is clicked.&lt;/p&gt;

&lt;p&gt;That gets us some basic functionality, but for my use case, I want to be able to programmatically open the dialog from the parent, not just when the toggle button is clicked. For that, we need a small refactor.&lt;/p&gt;

&lt;p&gt;To control the dialog from a parent, we need to add a new prop and trigger the dialog methods as it changes. Rather than repeat the open/close functionality for internal &lt;strong&gt;and&lt;/strong&gt; external changes let’s create a local state with &lt;a href="https://qwik.builder.io/docs/components/state/#usestore"&gt;&lt;code&gt;useStore()&lt;/code&gt;&lt;/a&gt; to track whether the dialog should be shown or not. Then we can simply toggle the state and respond to those changes using a &lt;a href="https://qwik.builder.io/docs/components/tasks/#tasks"&gt;Qwik task&lt;/a&gt; (it’s like &lt;code&gt;useEffect&lt;/code&gt; in React). If the &lt;code&gt;open&lt;/code&gt; prop from the parent changes, we need to respond to that change using another task, but this time we’ll use &lt;a href="https://qwik.builder.io/docs/components/tasks/#usevisibletask"&gt;&lt;code&gt;useVisibleTask$()&lt;/code&gt;&lt;/a&gt;. We also need to provide a way for the parent component to be aware of changes to the dialog’s visibility. We can do that by providing a custom &lt;code&gt;onClose$&lt;/code&gt; prop that will call the function whenever the dialog closes. And lastly, if we’re providing programmatic control from the parent, we may want to provide a way to hide the &lt;code&gt;&amp;lt;button&amp;gt;&lt;/code&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSignal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useTask$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useVisibleTask$&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;btnText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onClose$&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;dialogRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSignal&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;useTask&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;track&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="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&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;dialog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dialogRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;dialog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showModal&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;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nx"&gt;onClose$&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;onClose&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;useVisibleTask&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;track&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="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;open&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="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;false&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;btnText&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;onClick$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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="nx"&gt;btnText&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;)}&lt;/span&gt;

      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;
        &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;dialogRef&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;onClick$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dialog&lt;/span&gt;&lt;span class="dl"&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Slot&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/dialog&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="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;This is a lot better, but there’s still a little work to do. I like to make sure my components are &lt;a href="https://austingil.com/category/accessibility/"&gt;accessible&lt;/a&gt; and typed. In this case, since we’re using a button to control the visibility of the dialog, it makes sense to add &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-controls"&gt;&lt;code&gt;aria-controls&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-expanded"&gt;&lt;code&gt;aria-expanded&lt;/code&gt;&lt;/a&gt; attributes to the button. To connect them to the dialog, the dialog needs an ID, which we can either take from the props or dynamically generate. Lastly, pressing escape will close the dialog, but we also need to track the dialog’s native &lt;code&gt;"close"&lt;/code&gt; event by attaching an &lt;code&gt;onClose$&lt;/code&gt; handler.&lt;/p&gt;

&lt;p&gt;Here is my finished component, including &lt;a href="https://austingil.com/typescript-the-easy-way/"&gt;JSDoc type definitions&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;component$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSignal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useTask$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useVisibleTask$&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomString&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/utils.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * @typedef {HTMLAttributes&amp;lt;HTMLDialogElement&amp;gt;} DialogAttributes
 *
 * @type {Component&amp;lt;DialogAttributes  &amp;amp; {
 * toggle: string|false,
 * open?: Boolean,
 * onClose$?: import('@builder.io/qwik').PropFunction&amp;lt;() =&amp;gt; any&amp;gt;
 * }&amp;gt;}
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onClose$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;randomString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&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;dialogRef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSignal&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="nx"&gt;useTask&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;track&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="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&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;dialog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dialogRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&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;dialog&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;showModal&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;dialog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="nx"&gt;onClose$&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;onClose&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;useVisibleTask&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;track&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="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;open&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="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;open&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;false&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;toggle&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;controls&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;expanded&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;isOpen&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;onClick$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&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="nx"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;)}&lt;/span&gt;

      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;dialog&lt;/span&gt;
        &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;dialogRef&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;onClick$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localName&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dialog&lt;/span&gt;&lt;span class="dl"&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;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="p"&gt;}}&lt;/span&gt;
        &lt;span class="nx"&gt;onClose$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isOpen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;p-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Slot&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Slot&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/dialog&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="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;Cool! Now that we have a working dialog component, we need to put something in it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate AI Images with OpenAI
&lt;/h2&gt;

&lt;p&gt;Once the AI has determined a winner between opponent1 and opponent2, it would be cool to offer an image of them in combat. So why not add a button that says “Show me” after the results are available?&lt;/p&gt;

&lt;p&gt;After the text in the template, we could add a conditional like this:&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;winner&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;Show&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Awesome! It’s just too bad it doesn’t do anything…&lt;/p&gt;

&lt;p&gt;To actually generate the AI image, we need to make another API request to OpenAI, which means we need another API endpoint in our backend. We’ve already assigned a request handler to the current route. Let’s add a new route to handle GET requests to &lt;code&gt;/ai-image&lt;/code&gt; by adding a new file in &lt;code&gt;/src/routes/ai-image/index.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In many cases, a new route may need to return HTML from the server to generate a page. That’s not the case for this route. This will only ever return JSON, so it doesn’t need to be a JSX file or return a component.&lt;/p&gt;

&lt;p&gt;Instead, we can export a custom &lt;a href="https://qwik.builder.io/docs/middleware/"&gt;middleware&lt;/a&gt; like we did for post requests on the first page. To do that, we create a named export called &lt;code&gt;onGet&lt;/code&gt; that can look something like this:&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;const&lt;/span&gt; &lt;span class="nx"&gt;onGet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;some&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&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;Now, to get the route working, we need to do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grab opponent1 and opponent2 from the request (we’ll use query parameters).&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use the opponents to construct a prompt using LangChain templates.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a body for the OpenAI API request containing the prompt and the image size we want (I’ll use&lt;/strong&gt; &lt;code&gt;"512x512"&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create the authenticated HTTP request to OpenAI.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Respond to the initial request with the URL of the generated image.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For more details on working with images through OpenAI, refer to their &lt;a href="https://platform.openai.com/docs/api-reference/images"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Here’s how my implementation looks:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PromptTemplate&lt;/span&gt; &lt;span class="p"&gt;}&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;langchain/prompts&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;mods&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;cinematic&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;high resolution&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;epic&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;promptTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PromptTemplate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`{opponent1} and {opponent2} in a battle to the death, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;mods&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&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;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputVariables&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;opponent1&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;opponent2&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="cm"&gt;/** @type {import('@builder.io/qwik-city').RequestHandler} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onGet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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;OPENAI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="dl"&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;opponent1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;opponent1&lt;/span&gt;&lt;span class="dl"&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;opponent2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;opponent2&lt;/span&gt;&lt;span class="dl"&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;prompt&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;promptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opponent2&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;512x512&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/images/generations&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&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="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&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="nf"&gt;stringify&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="p"&gt;})&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&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="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;results&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="mi"&gt;0&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;It’s worth noting that the response from OpenAI should look something like this:&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1589478378&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;data&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&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;https://...&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;url&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;https://...&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also worth mentioning is the lack of validation for the opponents. It’s always a good idea to validate user input before processing it. Why don’t you try addressing that as an additional challenge?&lt;/p&gt;

&lt;h2&gt;
  
  
  Provide AI Images with Art Direction
&lt;/h2&gt;

&lt;p&gt;In the code example above, you may have noticed the &lt;code&gt;mods&lt;/code&gt; array that gets joined and appended to the end of the prompt. This is worth a callout.&lt;/p&gt;

&lt;p&gt;Generative images are tricky because the same prompt can return drastically different results. You might get a cartoon or an oil painting or a sketch. So it’ important to include a couple of hints to the AI to match the aesthetic of your application.&lt;/p&gt;

&lt;p&gt;I found that using an array with several different options allowed me to easily toggle off various features. In fact, in my actual project, I keep a much longer list of options, organized by style, format, quality, and effect.&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;mods&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="cm"&gt;/** Style */&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Abstract',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Academic',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Action painting',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Aesthetic',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Angular',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Automatism',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Avant-garde',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Baroque',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Bauhaus',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Contemporary',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Cubism',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Cyberpunk',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Digital art',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'photo',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'vector art',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Expressionism',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Fantasy',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Impressionism',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'kiyo-e',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Medieval',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Minimal',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Modern',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Pixel art',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Realism',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'sci-fi',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Surrealism',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'synthwave',&lt;/span&gt;
  &lt;span class="c1"&gt;// '3d-model',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'analog-film',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'anime',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'comic-book',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'enhance',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'fantasy-art',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'isometric',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'line-art',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'low-poly',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'modeling-compound',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'origami',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'photographic',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'tile-texture',&lt;/span&gt;

  &lt;span class="cm"&gt;/** Format */&lt;/span&gt;
  &lt;span class="c1"&gt;// '3D render',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Blender Model',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'CGI rendering',&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cinematic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Detailed render',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'oil painting',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'unreal engine 5',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'watercolor',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'cartoon',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'anime',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'colored pencil',&lt;/span&gt;

  &lt;span class="cm"&gt;/** Quality */&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;high resolution&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// 'high-detail',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'low-poly',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'photographic',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'photorealistic',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'realistic',&lt;/span&gt;

  &lt;span class="cm"&gt;/** Effects */&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Beautiful lighting',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Cinematic lighting',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Dramatic',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'dramatic lighting',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Dynamic lighting',&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;epic&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Portrait lighting',&lt;/span&gt;
  &lt;span class="c1"&gt;// 'Volumetric lighting',&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I’ve also found that it’s better to include these modifiers at the end of the prompt, otherwise they can be forgotten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request an AI-Generated Image
&lt;/h2&gt;

&lt;p&gt;Now that our endpoint is ready, we can start using it. We need to send an HTTP request with query parameters including opponent1 and opponent2. We can pull those values from the &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;s on demand, but I prefer to maintain some reactive state that gets updated any time a user types into the &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;s .&lt;/p&gt;

&lt;p&gt;Let’s modify our &lt;code&gt;state&lt;/code&gt; to include properties for opponent1 and opponent2:&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;text&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="na"&gt;winner&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="na"&gt;opponent1&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="na"&gt;opponent2&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, let’s add an &lt;code&gt;onInput$&lt;/code&gt; event handler that will update the state. The event handler should probably also clear any previous text results and winners. Note that we need to do this for both inputs.&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt;
  &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Opponent 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opponent1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;
    &lt;span class="na"&gt;rainbow&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="nx"&gt;winner&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;opponent1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}}&lt;/span&gt;
  &lt;span class="nx"&gt;onInput$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;winner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opponent1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="p"&gt;}}&lt;/span&gt;
&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have the values conveniently available, we can construct the HTTP request. We could do this when the “Show me” button is clicked, but we already made the &lt;code&gt;jsFormSubmit&lt;/code&gt; function in &lt;a href="https://austingil.com/ai-for-web-devs-streaming/"&gt;the third post of the series&lt;/a&gt;. Might as well reuse it. All it needs is a &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; with the data to send.&lt;/p&gt;

&lt;p&gt;Let’s create a form that submits to our &lt;code&gt;/ai-image&lt;/code&gt; route, prevents the default behavior, and submits the data with &lt;code&gt;jsFormSubmit&lt;/code&gt; instead. We can use hidden inputs to put the data in the form without impacting the UI.&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;winner&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;
    &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/ai-image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;preventdefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;
    &lt;span class="nx"&gt;onSubmit$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mt-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opponent1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;required&lt;/span&gt;
    &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hidden&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opponent2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
      &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nx"&gt;required&lt;/span&gt;
    &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;Show&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks the same as it did before, but now it actually does something. I would show you a screenshot, but it’s pretty much just a button. Unremarkable, but effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show the Image in the Dialog
&lt;/h2&gt;

&lt;p&gt;The last step is to put it all together.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The user submits the two opponents and the AI returns a winner.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The image generation&lt;/strong&gt; &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; will be available, showing the user the “Show me” button.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When the user clicks the button, the API request gets submitted.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;At the same time, we’ll programmatically open the dialog with some initial loading state.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When the request returns, we’ll display the image.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For that, I’ll create a new store for the image state using &lt;code&gt;useStore&lt;/code&gt;. It’ll hold a &lt;code&gt;showDialog&lt;/code&gt; state, an &lt;code&gt;isLoading&lt;/code&gt; state, and the &lt;code&gt;url&lt;/code&gt;. I’m also going to move the form’s submit handler into a dedicated function called &lt;code&gt;onSubmitImg&lt;/code&gt; so it’s not nested in the template and all the logic can live together.&lt;/p&gt;

&lt;p&gt;The body of the &lt;code&gt;obSubmitImg&lt;/code&gt; function will activate the dialog, set the loading state, submit the form with &lt;code&gt;jsFormSubmit&lt;/code&gt;, set the image URL from the results, and disable the loading state.&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;imgState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;showDialog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;url&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onSubmitImg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;showDialog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&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;results&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;
  &lt;span class="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Man, I love that &lt;code&gt;jsFormSubmit&lt;/code&gt; function! It lets the HTML provide the declarative HTTP logic and simplifies the business logic.&lt;/p&gt;

&lt;p&gt;Ok – with the state setup, the last thing to do is connect the &lt;code&gt;&amp;lt;Dialog&amp;gt;&lt;/code&gt; component. Since we’ll be opening it programmatically, we don’t need the built-in button. We can disable that by making the &lt;code&gt;btnText&lt;/code&gt; prop falsy. We can connect the &lt;code&gt;open&lt;/code&gt; prop to &lt;code&gt;imgState.showDialog&lt;/code&gt;. We’ll also want to update that state when the dialog closes via the &lt;code&gt;onClose$&lt;/code&gt; event. And the contents of the dialog should either show something for the loading state or show the generated image.&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Dialog&lt;/span&gt;
  &lt;span class="nx"&gt;btnText&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;open&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;showDialog&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;onClose$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;showDialog&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&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="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Working on it...&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="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;imgState&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;alt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;`An epic battle between &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="nx"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and &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="nx"&gt;opponent2&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="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Dialog&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, we now have a button that programmatically opens a dialog element without accessibility considerations. I thought about including the same ARIA attributes as we have on the toggle, but if I’m honest, I’m not sure if a submit button &lt;strong&gt;should&lt;/strong&gt; control a dialog.&lt;/p&gt;

&lt;p&gt;In this case, I’m leaving it out because I don’t know the right approach, and sometimes doing accessibility wrong leads to a worse experience than doing nothing at all. Open to suggestions. :)&lt;/p&gt;

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

&lt;p&gt;Okay, I think that’s as far as we’ll get today. It’s time to stop and enjoy the fruits of our labor.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--62djhM8V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://austingil.com/wp-content/uploads/ai-image-generation.gif%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--62djhM8V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://austingil.com/wp-content/uploads/ai-image-generation.gif%2520align%3D%2522left%2522" alt="An AI app determining the winner of a fight between a pirate and a ninja. It generates the text, &amp;quot;the ninja strikes again! While the pirate's peg leg got caught in the sand, the ninja stealthily swiped their hat, leaving the pirate completely discombobulated and unable to fight back. The ninja's quick movements and cunning tactics proved too slippery for the swashbuckling pirate to handle. Argh, better luck next time, matey!&amp;quot; then shows an AI generated image of a pirate and a ninja." width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Okay, OpenAI isn’t the best AI image generator, or maybe my prompt skills need some work. I’d love to see how yours turned out.&lt;/p&gt;

&lt;p&gt;A lot of the things we covered today were a review: building Qwik components, making HTTP requests to OpenAI, state, and conditional rendering in the template.&lt;/p&gt;

&lt;p&gt;The biggest difference, I think, is how we treat prompts for AI images. I find they require a little more creative thinking and finagling to get right. Hopefully, you found this helpful.&lt;/p&gt;

&lt;p&gt;In the video version, we covered a really cool SVG component that I think is worth checking out, but this post was already long enough.&lt;/p&gt;

&lt;p&gt;Functionally, the app is as far as I want to take it, so In the next post, we’ll focus on reliability and security before we get into launching to production.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/ai-for-web-devs-image-generation/"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;




</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI for Web Devs: Prompt Engineering</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Mon, 22 Jan 2024 18:38:47 +0000</pubDate>
      <link>https://dev.to/austingil/ai-for-web-devs-prompt-engineering-253k</link>
      <guid>https://dev.to/austingil/ai-for-web-devs-prompt-engineering-253k</guid>
      <description>&lt;p&gt;Welcome back to this series where we are building web applications that incorporate &lt;a href="https://austingil.com/category/ai/"&gt;AI&lt;/a&gt; tooling. &lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/"&gt;In the previous post&lt;/a&gt;, we covered what AI is, how it works, and some related terminology.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In this post, we’re going to cover prompt engineering, which is a way to modify your application’s behavior without changing the code. Since it’s challenging to explain without seeing the code, let’s get to it.&lt;/p&gt;

&lt;p&gt;%[&lt;a href="https://www.youtube.com/watch?v=pK6WzlTOlYw"&gt;https://www.youtube.com/watch?v=pK6WzlTOlYw&lt;/a&gt;] &lt;/p&gt;

&lt;h2&gt;
  
  
  Start Adapting the UI
&lt;/h2&gt;

&lt;p&gt;I hope you’ve come up with your own idea for an AI app, because this is where we’ll write mostly the same code, but could end up with different apps.&lt;/p&gt;

&lt;p&gt;My app will take two different opponents and tell you who would win in a fight. I’ll start on the UI side of things because that’s easier for me.&lt;/p&gt;

&lt;p&gt;So far, we’ve been giving users a single &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; and expecting them to write the entire prompt body to send to OpenAI. We can reduce the work users need to do and get more accurate prompts by modifying the UI to only ask for the missing details instead of the whole prompt.&lt;/p&gt;

&lt;p&gt;In my app’s case, we really only need two things: opponent 1 and opponent 2. So instead of one input, we’ll have two.&lt;/p&gt;

&lt;p&gt;This is a good opportunity to replace the &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; &lt;a href="https://austingil.com/category/html/"&gt;HTML&lt;/a&gt; with a reusable input component.&lt;/p&gt;

&lt;p&gt;I’ll add a file called &lt;code&gt;Input.jsx&lt;/code&gt; to the &lt;code&gt;/src/components&lt;/code&gt; folder. The most basic example of a Qwik component is a function that uses the &lt;code&gt;component$&lt;/code&gt; function from &lt;code&gt;"@&lt;/code&gt;&lt;a href="https://austingil.com/category/ai/"&gt;&lt;code&gt;builder.io/qwik&lt;/code&gt;&lt;/a&gt;&lt;code&gt;"&lt;/code&gt; and returns JSX.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;props&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;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;Our &lt;code&gt;Input&lt;/code&gt; component should be reusable and accessible. For that, it needs a required &lt;code&gt;label&lt;/code&gt; prop, a required &lt;code&gt;name&lt;/code&gt; attribute, and an optional &lt;code&gt;id&lt;/code&gt; which will default to a random string if not provided. And any other HTML attribute can be applied directly on the form control.&lt;/p&gt;

&lt;p&gt;Here’s what I came up with along with &lt;a href="https://austingil.com/typescript-the-easy-way/"&gt;JSDocs type definitions&lt;/a&gt; (note that the &lt;code&gt;randomString&lt;/code&gt; function comes from &lt;a href="https://github.com/AustinGil/utils/blob/master/js/randomString.js"&gt;this utility repo&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomString&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;~/utils.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * @typedef {import("@builder.io/qwik").HTMLAttributes&amp;lt;HTMLTextAreaElement&amp;gt;} TextareaAttributes
 */&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * @type {import("@builder.io/qwik").Component&amp;lt;TextareaAttributes  &amp;amp; {
 * label: string,
 * name: string,
 * id?: string,
 * value?: string
 * }&amp;gt;}
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&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="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&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;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nf"&gt;randomString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="o"&gt;=&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="o"&gt;&amp;gt;&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/label&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&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="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;props&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/textarea&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="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;It’s rudimentary, but works for our app. If you’re feeling spunky, I encourage you to modify it to support the other input and select elements.&lt;/p&gt;

&lt;p&gt;Now, instead of using a single &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; for the whole prompt, we can replace it with one of our new Input components for each opponent. I’ll put them in a two-column grid, so they sit next to each other on large screens.&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grid gap-4 sm:grid-cols-2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Opponent 1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opponent1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Opponent 2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;opponent2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Side-Quest: &lt;code&gt;global.d.ts&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;If you’re interested in using TypeScript or JSDocs, it may be useful to make the Qwik &lt;code&gt;HTMLAttributes&lt;/code&gt; and &lt;code&gt;Component&lt;/code&gt; global declarations so they’re easier to use across the application.&lt;/p&gt;

&lt;p&gt;To do that, create a file at &lt;code&gt;./src/global.d.ts&lt;/code&gt;. Inside it, we’ll import &lt;code&gt;HTMLAttributes&lt;/code&gt; and &lt;code&gt;Component&lt;/code&gt; from &lt;code&gt;"@&lt;/code&gt;&lt;a href="https://austingil.com/category/ai/"&gt;&lt;code&gt;builder.io/qwik&lt;/code&gt;&lt;/a&gt;&lt;code&gt;"&lt;/code&gt; with aliases, then create global declarations with their original names that implement their functionality:&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;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;QC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;HTMLAttributes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;QH&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="nx"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;QC&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;HTMLAttributes&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;QH&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is just an optional step, but I like to do it because I use these two type definitions frequently. It’s nice to not have to import them all the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adjust the Backend
&lt;/h2&gt;

&lt;p&gt;Now that we’ve changed our UI to reduce the amount of information we ask for, we can move to the backend.&lt;/p&gt;

&lt;p&gt;In the previous version, we were sending the entire prompt content using a form field named “prompt”. Now, we’re sending the two individual opponents, and we need to construct the prompt in the request handler.&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;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&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;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseBody&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;opponent1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;opponent2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Who would win in a fight between &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?`&lt;/span&gt;

  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Functionally, this brings us back to where we were, except now there’s less work for the user to do and they have better guidance on what they need. That’s great! Unfortunately, the AI response is still something like, “As an AI language model I can’t predict hypothetical fights or determined definite winners blah blah blah…”&lt;/p&gt;

&lt;p&gt;It’s not very helpful.&lt;/p&gt;

&lt;p&gt;But because we’ve moved the control of the prompt to the backend, we’ve set the stage for prompt engineering because now we are in control of it instead of the user.&lt;/p&gt;

&lt;p&gt;Here’s where our apps can take wildly different routes depending on how closely you want to follow the prompt that I write or if you make your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Begin Prompt Engineering
&lt;/h2&gt;

&lt;p&gt;The AI already told us that there is no way it could know who could win in a fight, but what if we’re a little more persuasive? Let’s change our prompt to something like this:&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Who would win in a fight between &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?

Provide a creative and detailed explanation of why they would win and what tactics they might use.`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, instead of asking the AI for a wishy-washy answer, we’re encouraging it to provide a creative explanation. The result?&lt;/p&gt;

&lt;p&gt;“In a hypothetical battle between a ninja and a pirate, the outcome would depend on several factors. Both ninjas and pirates possess unique skill sets and tactics that make them formidable opponents, so let’s imagine a thrilling encounter between the two and explore the possible outcome…”&lt;/p&gt;

&lt;p&gt;That’s much better!&lt;/p&gt;

&lt;p&gt;Of course, it’s going to be different each time, so I don’t expect you to get the same results, but the key thing is that the AI is cooperating.&lt;/p&gt;

&lt;h2&gt;
  
  
  Character Building
&lt;/h2&gt;

&lt;p&gt;Our app is mostly working now, but I think we can also make it more interesting. One way to do that is to give the AI some context about the role it should play as it answers the questions. For example, why not make it answer questions as if it were a professional fighting judge from Liverpool who speaks mostly with Cockney slang?&lt;/p&gt;

&lt;p&gt;To do that, we simply need to modify our prompt, but I also like to break up my prompt into various sections so it’s easier to manage.&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;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`You're a professional fighting judge from Liverpool that speaks mostly with Cockney slang`&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Who would win in a fight between &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; and &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?`&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Provide a creative and detailed explanation of why they would win and what tactics they might use.`&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;question&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;format&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, each separate section is captured by its own variable, which makes things easier for me to follow when I look at this later on.&lt;/p&gt;

&lt;p&gt;What’s the result?&lt;/p&gt;

&lt;p&gt;“Alright, mate! Let me put on me Cockney cap and dive into this lively debate between a ninja and a pirate. Picture meself in Liverpool, surrounded by kickin’ brick walls, ready to analyze this rumble in the most creative way…”&lt;/p&gt;

&lt;p&gt;It spits out over three thousand words of ridiculousness, which is a lot of fun, but highlights another problem. The output is too long.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Tokens
&lt;/h2&gt;

&lt;p&gt;Something worth understanding with these AI tools is “tokens”. From the OpenAI help article, “&lt;a href="https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them"&gt;What are tokens and how to count them?&lt;/a&gt;“:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;“Tokens can be thought of as pieces of words. Before the API processes the prompts, the input is broken down into tokens. These tokens are not cut up exactly where the words start or end – tokens can include trailing spaces and even sub-words.”&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A token accounts for roughly four characters, they are calculated based on the text that the AI receives and produces, and there are two big reasons we need to be aware of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The platform charges based on the volume of tokens used.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Each LLM has a limit on the maximum tokens it can work with.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So it’s worth being cognizant of the length of text we send as a prompt as well as what we receive as a response. In some cases, you may want a lengthy response to achieve a better product, but otherwise, in other cases, it’s better to use fewer tokens.&lt;/p&gt;

&lt;p&gt;In our case, a three thousand character response is not only a less-than-ideal user experience, it’s also costing us more money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reducing Tokens
&lt;/h2&gt;

&lt;p&gt;Now that we’ve decided to reduce the tokens we use, the next question is, how?&lt;/p&gt;

&lt;p&gt;If you’ve read through the OpenAI docs, you may have noticed a &lt;a href="https://platform.openai.com/docs/api-reference/completions/create#completions-create-max_tokens"&gt;&lt;code&gt;max_tokens&lt;/code&gt;&lt;/a&gt; parameter that we can set when we make the API request. Also, good on you for reading the docs. Five stars.&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&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="nf"&gt;stringify&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s see what happens when we set the &lt;code&gt;max_tokens&lt;/code&gt; parameter to something like 100.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SxNVmNMk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/image-94-1080x262.png%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SxNVmNMk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/image-94-1080x262.png%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" alt="Screenshot with the text, &amp;quot;Alright mate, let's 'ave a go at this one, shall we? So we've got a pirate versus a ninja in a good ol' scrap, eh? Well, let's break it down then, me old china. First off, let's talk about the pirate, savvy? Pirates are known for their fierce nature, dirty tricks, and that pirate code they live by. They're tough blokes, with years of seafaring experience under their belts. With their cunning minds,&amp;quot;" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ok, now this is about the right length that I want, but it looks like it’s getting cut off. That’s because the GPT was given a hard limit on how much it could return, but it doesn’t account for that when constructing the response. As a result, we end up with an incomplete thought.&lt;/p&gt;

&lt;p&gt;Not ideal.&lt;/p&gt;

&lt;p&gt;Programmatically limiting the allowed length probably makes sense in some applications. It may even make sense in this one to add an upper bound. But to get a short AND complete response, the solution comes back to prompt engineering.&lt;/p&gt;

&lt;p&gt;Let’s modify our prompt to ask for a “short explanation” instead of a “creative and detailed” one.&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;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Only tell me who would win and a short reason why.`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--e_TqACum--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/image-95-1080x315.png%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--e_TqACum--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/image-95-1080x315.png%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" alt="Screenshot with the text, &amp;quot;Oi, mate! In me professional opinion, it's the ninja who'd come out on top in this fight, no doubt about it. You see, ninjas are skilled stealthy buggers, highly trained in combat and quick on their feet. They know 'ow to use their surroundings to their advantage and got sneaky moves up their sleeve. While pirates may be tough ol' lads, their brute force and love for rum won't be enough to outsmart a ninja's tactics and precision. So, puttin' it plain and simple, the ninja is likely to dance circles around that pirate and give 'em a jab or two before they even know what 'it 'em. Winner: The Ninja, hands down, or should I say, swords up!&amp;quot;" width="800" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Okay, this is more like what I had in mind. This is about the right length and level of detail. If you want to massage it some more, I encourage you to do so, but I’m going to move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introducing LangChain
&lt;/h2&gt;

&lt;p&gt;I want to address the clunkiness of the current system. You can imagine if we had a lot more prompts and a lot more endpoints it might be hard to manage. That’s why I want to introduce a tool chain called &lt;a href="https://js.langchain.com/docs/get_started/introduction"&gt;LangChain&lt;/a&gt;. In this new and constantly shifting world of AI, it’s been emerging as the leading tool chain for working with prompts. Let’s see why.&lt;/p&gt;

&lt;p&gt;First, install the package with &lt;code&gt;npm install @langchain/core&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The most relevant thing we can do with LangChain for our project is to generate prompts using &lt;a href="https://js.langchain.com/docs/modules/model_io/prompts/prompt_templates/"&gt;prompt templates&lt;/a&gt;. Instead of generating our prompt from within our route handler, we can create a shareable prompt template and only provide the variables (opponent 1 &amp;amp; 2) at runtime. It’s essentially a factory function for prompts.&lt;/p&gt;

&lt;p&gt;We can import the &lt;code&gt;PromptTeplate&lt;/code&gt; module from &lt;code&gt;"@langchain/core/prompts"&lt;/code&gt;, then create a template and configure any variables it will consume like this:&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;promptTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PromptTemplate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;inputVariables&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;opponent1&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;opponent2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You're a professional fighting judge from Liverpool that speaks mostly with Cockney slang. Who would win in a fight between {opponent1} and {opponent2}? Only tell me who would win and a short reason why.`&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;Notice that we’re using two &lt;code&gt;inputVariables&lt;/code&gt; called “opponent1” and “opponent2”. These will be referenced in the template inside curly braces. It tells LangChain what variables to expect at runtime and where to place them.&lt;/p&gt;

&lt;p&gt;So now, within our route handler, instead of constructing the entire prompt, we can call &lt;code&gt;promptTemplate.format&lt;/code&gt; and provide our variables.&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;prompt&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;promptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opponent1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;opponent2&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Separating our prompt template from the route handler’s business logic simplifies the handler, makes the template easier to maintain, and allows us to export and share the template across the codebase if needed.&lt;/p&gt;

&lt;p&gt;It’s worth mentioning that prompt templates are not the only benefit that LangChain offers. They also have tooling for managing the memory in chat applications, caching, handling timeouts, rate limiting, and more. This is just an introduction, but it’s worth getting more familiar with the capabilities if you plan on going deeper.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identifying the Winner
&lt;/h2&gt;

&lt;p&gt;One last thing that I want to do before we finish up today is to highlight the winner based on the response. Unfortunately, it’s hard to know that from a large block of indeterminate text.&lt;/p&gt;

&lt;p&gt;Now, you may be thinking it would be nice to use a JSON object containing the winner and the text, and you’d be right.&lt;/p&gt;

&lt;p&gt;Just one problem, in order to parse JSON, we need the entire JSON string, which means we would need to wait until the entire text completes. This kind of defeats the purpose of streaming.&lt;/p&gt;

&lt;p&gt;This was one of the tricky challenges I found dealing with AI APIs.&lt;/p&gt;

&lt;p&gt;The solution I came up with was to format the streaming response like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;winner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;opponent1 &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;or&lt;/span&gt; &lt;span class="nx"&gt;opponent2&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="nx"&gt;they&lt;/span&gt; &lt;span class="nx"&gt;won&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This way, I could grab the winner programmatically and continue writing the reason to the page as it arrived by skipping the unrelated text. I’d love to hear your thoughts or see what you come up with, but let’s see how this worked.&lt;/p&gt;

&lt;p&gt;First, we need to modify the prompt. In order for the AI to know how to respond with the winner, both opponents need a label (“opponent1” and “opponent2”). We’ll add those labels in parentheses when we first mention the opponents. And since we have a more specific requirement on what the returned format needs to be, we should also include that in the template.&lt;/p&gt;

&lt;p&gt;Here’s what my template looks like now:&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="s2"&gt;`You're a professional fighting judge from Liverpool that speaks mostly with Cockney slang. Who would win in a fight between {opponent1} ("opponent1") and {opponent2}("opponent2")? Only tell me who would win and a short reason why.

Format the response like this:
"winner: 'opponent1' or 'opponent2'. reason: the reason they won."`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how now I’m giving the AI an example of what the response should look like. This is sometimes referred to as a one-shot prompt. What we had before without any example would be a zero-shot prompt. You can also have a multi-shot where you provide multiple examples.&lt;/p&gt;

&lt;p&gt;OK, so now we should get back some text that tells us who the winner is and the reasoning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fg1efa4G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/image-96.png%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fg1efa4G--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/image-96.png%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" alt="winner: opponent2. reason: They possess a combination of stealth, agility, and lethal combat skills that make them formidable opponents in close-quarters combat. Their ability to strike swiftly and silently gives them a significant advantage over the pirate." width="800" height="125"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The last step is to modify the way the frontend deals with this response so we separate the winner from the reasoning.&lt;/p&gt;

&lt;p&gt;Showing just the reason to the user is the easy part. The first bit of the response will always be “&lt;code&gt;winner: opponent1 (or 2). reason:&lt;/code&gt; “. So we can store the whole string in state, but skip the first 27 characters and show just the reason to the user. There are certainly some more advanced ways to get just the reasoning, but sometimes I prefer a simple solution.&lt;/p&gt;

&lt;p&gt;We can replace this:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this:&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Identifying the winner is a little more robust. When the streaming response comes back, it still gets pushed to &lt;code&gt;state.text&lt;/code&gt;. And after the response completes, we can pluck the winner from the results. You could slice the string, but I chose to use a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions"&gt;Regular Expression&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="c1"&gt;// Previous fetch request logic&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;winnerPattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/winner:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(\w&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;.*/gi&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;winnerPattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&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="nx"&gt;text&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;winner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This Regular Expression looks for a string beginning with “winner:”, has an optional white-space character, then captures the next whole word up until a period character. Compared to our template, the captured word should either be “opponent1” or “opponent2”, our winners ;)&lt;/p&gt;

&lt;p&gt;Once you have the winner, what you do with that information is up to you. I thought it would be cool to store it in state, and apply a fun rainbow background animation and confetti explosion (&lt;a href="https://www.npmjs.com/package/party-js"&gt;&lt;code&gt;party-js&lt;/code&gt;&lt;/a&gt;) to the corresponding &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; .&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VREh9DU9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/pirate-vs-ninja.gif%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VREh9DU9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/pirate-vs-ninja.gif%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" alt="Animated gif showing the user asking the app who woudl win between a pirate and a ninja. The app responds with streaming text saying the ninja would win then adding an animated rainbow background and exploding confetti to the ninja text box." width="800" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That’s so fun. I love it!&lt;/p&gt;

&lt;p&gt;I’ll let you sort that out if you want to recreate it, but here’s some of the code in case you’re interested.&lt;/p&gt;

&lt;p&gt;JS:&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;if &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="nx"&gt;winner&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;winnerInput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`textarea[name=&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="nx"&gt;winner&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;winnerInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;party&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;confetti&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;winnerInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;size&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;spread&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&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;CSS:&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="nx"&gt;rainbow&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="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;linear&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="nx"&gt;deg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;cc0000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;c8cc00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;38&lt;/span&gt;&lt;span class="nx"&gt;cc00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;00&lt;/span&gt;&lt;span class="nx"&gt;ccb5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;0015&lt;/span&gt;&lt;span class="nx"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="nx"&gt;f00cc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;c200cc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;#&lt;/span&gt;&lt;span class="nx"&gt;cc0000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1600&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;BgSlide&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="nx"&gt;linear&lt;/span&gt; &lt;span class="nx"&gt;infinite&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="nd"&gt;keyframes&lt;/span&gt; &lt;span class="nx"&gt;BgSlide&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;50&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="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;background&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Review
&lt;/h2&gt;

&lt;p&gt;Alright, in the end, we did get into a few code changes, but I don’t want that to overshadow the main focus of this article. Now, we can drastically change the behavior of our app just by tweaking the prompt.&lt;/p&gt;

&lt;p&gt;Some things we covered were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Providing the AI with some context about its role.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Formatting responses.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The importance of understanding tokens.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tooling like LangChain&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zero-shot, one-shot, and n-shot prompts.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I also don’t want to understate how much work can go into getting a prompt just right. This post was a silly example, but it actually took me a very long time to figure out the right permutations of words and formats to get what I needed. Don’t feel bad if it takes you a while to get used to it as well.&lt;/p&gt;

&lt;p&gt;I truly believe that becoming a good prompt engineer will serve you well in the future. Even if you’re not building apps, it’s helpful for interacting with GPTs. But if you’re building apps, the key differentiating factors between the winners and losers will be the secret sauce that goes into the prompts &lt;strong&gt;and&lt;/strong&gt; the form factor of using the app. It will need to be intuitive and provide the user with the least friction to get what they want.&lt;/p&gt;

&lt;p&gt;In the next post we’ll start playing around with AI image generation, which comes with its own fun and quirky experience.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I hope you stick around, and feel free to reach out any time.&lt;/p&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/category/ai/"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AI for Web Devs: What Are Neural Networks, LLMs, &amp; GPTs?</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Thu, 18 Jan 2024 23:34:19 +0000</pubDate>
      <link>https://dev.to/austingil/ai-for-web-devs-what-are-neural-networks-llms-gpts-2bb5</link>
      <guid>https://dev.to/austingil/ai-for-web-devs-what-are-neural-networks-llms-gpts-2bb5</guid>
      <description>&lt;p&gt;Welcome back to &lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;this series&lt;/a&gt; where we’re building web applications with AI tooling.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/" rel="noopener noreferrer"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/" rel="noopener noreferrer"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/" rel="noopener noreferrer"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/" rel="noopener noreferrer"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/" rel="noopener noreferrer"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In &lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;the previous post&lt;/a&gt;, we got &lt;a href="https://austingil.com/category/ai/" rel="noopener noreferrer"&gt;AI&lt;/a&gt; generated jokes into our &lt;a href="https://qwik.builder.io/" rel="noopener noreferrer"&gt;Qwik&lt;/a&gt; application from &lt;a href="https://platform.openai.com/" rel="noopener noreferrer"&gt;OpenAI API&lt;/a&gt;. It worked, but the user experience suffered because we had to wait until the API completed the entire response before updating the client.&lt;/p&gt;

&lt;p&gt;A better experience, as you’ll know if you’ve used any AI chat tools, is to respond as soon as each bit of text is generated. It becomes a sort of teletype effect.&lt;/p&gt;

&lt;p&gt;That’s what we’re going to build today using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Streams_API" rel="noopener noreferrer"&gt;HTTP streams&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/GkyHBwUA0EQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we get into streams, we need to explore something with a Qwik quirk related to HTTP requests.&lt;/p&gt;

&lt;p&gt;If we examine the current POST request being sent by the form, we can see that the returned payload isn’t just the plain text we returned from our action handler. Instead, it’s this sort of &lt;a href="https://qwik.builder.io/docs/guides/serialization/#serialization-boundary" rel="noopener noreferrer"&gt;serialized data&lt;/a&gt;.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-92-1080x612.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-92-1080x612.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Screenshot of Chrome devtools Network tab with a serialized reponse from the Qwik backend"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the result of how the &lt;a href="https://qwik.builder.io/docs/advanced/optimizer/" rel="noopener noreferrer"&gt;Qwik Optimizer&lt;/a&gt; lazy loads assets and is necessary to properly handle the data as it comes back. Unfortunately, this prevents standard streaming responses.&lt;/p&gt;

&lt;p&gt;So while &lt;a href="https://qwik.builder.io/docs/action/#routeaction" rel="noopener noreferrer"&gt;&lt;code&gt;routeAction$&lt;/code&gt;&lt;/a&gt; and the &lt;code&gt;Form&lt;/code&gt; component are super handy, we’ll have to do something else.&lt;/p&gt;

&lt;p&gt;To their credit, the Qwik team does provide a &lt;a href="https://qwik.builder.io/docs/server$/#streaming-responses" rel="noopener noreferrer"&gt;well-documented approach for streaming responses&lt;/a&gt;. However, it involves their &lt;code&gt;server$&lt;/code&gt; function and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator" rel="noopener noreferrer"&gt;async generator functions&lt;/a&gt;. This would probably be the right approach if we’re talking strictly about Qwik, but this series is for everyone. I’ll avoid this implementation, as it’s too specific to Qwik, and focus on broadly applicable concepts instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactor Server Logic
&lt;/h2&gt;

&lt;p&gt;It sucks that we can’t use route actions because they’re great. So what can we use?&lt;/p&gt;

&lt;p&gt;Qwik City offers a few options. The best I found is &lt;a href="https://qwik.builder.io/docs/middleware/#middleware" rel="noopener noreferrer"&gt;middleware&lt;/a&gt;. They provide enough access to primitive tools that we can accomplish what we need, and the concepts will apply to other contexts besides Qwik.&lt;/p&gt;

&lt;p&gt;Middleware is essentially a set of functions that we can inject at various points within the request lifecycle of our route handler. We can define them by exporting named constants for the hooks we want to target (&lt;code&gt;onRequest&lt;/code&gt;, &lt;code&gt;onGet&lt;/code&gt;, &lt;code&gt;onPost&lt;/code&gt;, &lt;code&gt;onPut&lt;/code&gt;, &lt;code&gt;onDelete&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;So instead of relying on a route action, we can use a middleware that hooks into any POST request by exporting an &lt;code&gt;onPost&lt;/code&gt; middleware. In order to support streaming, we’ll want to return a standard &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Response" rel="noopener noreferrer"&gt;Response&lt;/a&gt; object. We can do so by creating a Response object and passing it to the &lt;a href="https://qwik.builder.io/docs/middleware/#send" rel="noopener noreferrer"&gt;&lt;code&gt;requestEvent.send()&lt;/code&gt;&lt;/a&gt; method.&lt;/p&gt;

&lt;p&gt;Here’s a basic (non-streaming) example:&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="cm"&gt;/** @type {import('@builder.io/qwik-city').RequestHandler} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello Squirrel!&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;Before we tackle streaming, let’s get the same functionality from the old route action implemented with middleware. We can copy most of the code into the &lt;code&gt;onPost&lt;/code&gt; middleware, but we won’t have access to &lt;code&gt;formData&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fortunately, we can recreate that data from the &lt;a href="https://qwik.builder.io/docs/middleware/#parsebody" rel="noopener noreferrer"&gt;&lt;code&gt;requestEvent.parseBody()&lt;/code&gt;&lt;/a&gt; method. We’ll also want to use &lt;code&gt;requestEvent.send()&lt;/code&gt; to respond with the OpenAI data instead of a &lt;code&gt;return&lt;/code&gt; statement.&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="cm"&gt;/** @type {import('@builder.io/qwik-city').RequestHandler} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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;OPENAI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="dl"&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;formData&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;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseBody&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="c1"&gt;// ... fetch options&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="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="nf"&gt;json&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;responseBody&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="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;

  &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responseBody&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;
  
  
  Refactor Client Logic
&lt;/h2&gt;

&lt;p&gt;Replacing the route actions has the unfortunate side effect of meaning we also can’t use the &lt;code&gt;&amp;lt;Form&amp;gt;&lt;/code&gt; component anymore. We’ll have to use a regular &lt;a href="https://austingil.com/category/html/" rel="noopener noreferrer"&gt;HTML&lt;/a&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;&lt;/a&gt; element and recreate all the benefits we had before, including sending HTTP request with &lt;a href="https://austingil.com/category/javascript/" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt;, tracking the loading state, and accessing the results. Let’s refactor our client-side to support those features again.&lt;/p&gt;

&lt;p&gt;We can break these requirements down to needing two things, a JavaScript solution for submitting forms and a reactive state for managing loading states and results.&lt;/p&gt;

&lt;p&gt;I’ve covered submitting HTML forms with JavaScript in depth several times in the past:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/resilient-applications-progressive-enhancement/" rel="noopener noreferrer"&gt;&lt;strong&gt;Make Beautifully Resilient Apps With Progressive Enhancement&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/upload-files-with-javascript/" rel="noopener noreferrer"&gt;&lt;strong&gt;How to Upload Files with JavaScript&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=ATqDrIkA8fA&amp;amp;t=1s" rel="noopener noreferrer"&gt;&lt;strong&gt;Building Super Powered HTML Forms with JavaScript&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So today I’ll just share the snippet, which I put inside a &lt;code&gt;utils.js&lt;/code&gt; file in the root of my project. This &lt;code&gt;jsFormSubmit&lt;/code&gt; function accepts an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement" rel="noopener noreferrer"&gt;&lt;code&gt;HTMLFormElement&lt;/code&gt;&lt;/a&gt; then constructs a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API" rel="noopener noreferrer"&gt;&lt;code&gt;fetch&lt;/code&gt;&lt;/a&gt; request based on the form attributes and returns the resulting &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise" rel="noopener noreferrer"&gt;Promise&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="cm"&gt;/**
 * @param {HTMLFormElement} form 
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&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;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&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;searchParameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="cm"&gt;/** @type {Parameters&amp;lt;typeof fetch&amp;gt;[1]} */&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&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;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;post&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;fetchOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enctype&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multipart/form-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchParameters&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParameters&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchOptions&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;This generic function can be used to submit any HTML form, so it’s handy to use in a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit_event" rel="noopener noreferrer"&gt;&lt;code&gt;submit&lt;/code&gt; event&lt;/a&gt; handler. Sweet!&lt;/p&gt;

&lt;p&gt;As for the reactive data, Qwik provides two options, &lt;a href="https://qwik.builder.io/docs/components/state/#usestore" rel="noopener noreferrer"&gt;&lt;code&gt;useStore&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://qwik.builder.io/docs/components/state/#usesignal" rel="noopener noreferrer"&gt;&lt;code&gt;useSignal&lt;/code&gt;&lt;/a&gt;. I prefer &lt;code&gt;useStore&lt;/code&gt;, which allows us to create an object whose properties are &lt;a href="https://qwik.builder.io/docs/concepts/reactivity/" rel="noopener noreferrer"&gt;reactive&lt;/a&gt;. Meaning changes to the object’s properties will automatically be reflected wherever they are referenced in the UI.&lt;/p&gt;

&lt;p&gt;We can use &lt;code&gt;useStore&lt;/code&gt; to create a “state” object in our component to track the loading state of the HTTP request as well as the text response.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// other setup logic&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&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="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// other component logic&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we can update the template. Since we can no longer use the &lt;code&gt;action&lt;/code&gt; object we had before, we can replace references from &lt;code&gt;action.isRunning&lt;/code&gt; and &lt;code&gt;action.value&lt;/code&gt; to &lt;code&gt;state.isLoading&lt;/code&gt; and &lt;code&gt;state.text&lt;/code&gt;, respectively (don’t ask me why I changed the property names 🤷‍♂️). I’ll also add a “submit” event handler to the form called &lt;code&gt;handleSbumit&lt;/code&gt;, which we’ll look at shortly.&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; 
    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;preventdefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;
    &lt;span class="nx"&gt;onSubmit$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Prompt&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/label&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;Tell&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;joke&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/textarea&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;isLoading&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;One sec...&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;Tell me&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
  &lt;span class="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="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/article&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/main&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; does not explicitly provide an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#action" rel="noopener noreferrer"&gt;&lt;code&gt;action&lt;/code&gt;&lt;/a&gt; attribute. By default, an HTML form will submit data to the current URL, so we only need to set the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#method" rel="noopener noreferrer"&gt;&lt;code&gt;method&lt;/code&gt;&lt;/a&gt; to POST and submit this form to trigger the &lt;code&gt;onPost&lt;/code&gt; middleware we defined earlier.&lt;/p&gt;

&lt;p&gt;Now, the last step to get this refactor working is defining &lt;code&gt;handleSubmit&lt;/code&gt;. Just like we did in &lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;the previous post&lt;/a&gt;, we need to wrap an event handler inside Qwik’s &lt;code&gt;$&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Inside the event handler, we’ll want to clear out any previous data from &lt;code&gt;state.text&lt;/code&gt;, set &lt;code&gt;state.isLoading&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, then pass the form’s DOM node to our fancy &lt;code&gt;jsFormSubmit&lt;/code&gt; function. This should submit the HTTP request for us. Once it comes back, we can update &lt;code&gt;state.text&lt;/code&gt; with the response body, and return &lt;code&gt;state.isLoading&lt;/code&gt; to &lt;code&gt;false&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="cm"&gt;/** @type {HTMLFormElement} */&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&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="nx"&gt;text&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="nf"&gt;text&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="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OK! We should now have a client-side form that uses JavaScript to submit an HTTP request to the server while tracking the loading and response states, and updating the UI accordingly.&lt;/p&gt;

&lt;p&gt;That was a lot of work to get the same solution we had before but with fewer features. BUT the key benefit is we now have direct access to the platform primitives we need to support streaming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Streaming on the Server
&lt;/h2&gt;

&lt;p&gt;Before we start streaming responses from OpenAI, I think it’s helpful to start with a very basic example to get a better grasp of streams. Streams allow us to send small chunks of data over time. So as an example, let’s print out some iconic David Bowie lyrics in tempo with the song, “&lt;a href="https://www.youtube.com/watch?v=iYYRH4apXDo" rel="noopener noreferrer"&gt;Space Oddity&lt;/a&gt;“.&lt;/p&gt;

&lt;p&gt;When we construct our Response object, instead of passing plain text, we’ll want to pass a stream. We’ll create the stream shortly, but here’s the idea:&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="cm"&gt;/** @type {import('@builder.io/qwik-city').RequestHandler} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&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 create a very rudimentary &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream" rel="noopener noreferrer"&gt;&lt;code&gt;ReadableStream&lt;/code&gt;&lt;/a&gt; using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream" rel="noopener noreferrer"&gt;&lt;code&gt;ReadableStream&lt;/code&gt; constructor&lt;/a&gt; and pass it an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream#parameters" rel="noopener noreferrer"&gt;optional parameter&lt;/a&gt;. This optional parameter can be an object with a &lt;code&gt;start&lt;/code&gt; method that’s called when the stream is constructed.&lt;/p&gt;

&lt;p&gt;The start method is responsible for the steam’s logic and has access to the stream &lt;code&gt;controller&lt;/code&gt;, which is used to send data and close the stream.&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stream logic goes here&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;OK, let’s plan out that logic. We’ll have an array of song lyrics and a function to ‘sing’ them (pass them to the stream). The &lt;code&gt;sing&lt;/code&gt; function will take the first item in the array and pass that to the stream using the &lt;code&gt;controller.enqueue()&lt;/code&gt; method. If it’s the last lyric in the list, we can close the stream with &lt;code&gt;controller.close()&lt;/code&gt;. Otherwise, the &lt;code&gt;sing&lt;/code&gt; method can call itself again after a short pause.&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&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;lyrics&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;Ground&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; control&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; to major&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; Tom.&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="nf"&gt;sing&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;lyric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lyrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

      &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lyric&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;lyrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&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="nf"&gt;sing&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;So each second, for four seconds, this stream will send out the lyrics “Ground control to major Tom.” Slick!&lt;/p&gt;

&lt;p&gt;Because this stream will be used in the body of the Response, the connection will remain open for four seconds until the response completes. But the frontend will have access to each chunk of data as it arrives, rather than waiting the full four seconds.&lt;/p&gt;

&lt;p&gt;This doesn’t speed up the total response time (in some cases, streams can increase response times), but it does allow for a faster &lt;strong&gt;perceived&lt;/strong&gt; response, and that makes a better user experience.&lt;/p&gt;

&lt;p&gt;Here’s what my code looks like:&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="cm"&gt;/** @type {import('@builder.io/qwik-city').RequestHandler} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequestHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&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;lyrics&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;Ground&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; control&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; to major&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; Tom.&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="nf"&gt;sing&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;lyric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lyrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lyric&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;lyrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&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="nf"&gt;sing&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;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&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;Unfortunately, as it stands right now, the client will still be waiting four seconds before seeing the entire response, and that’s because we weren’t expecting a streamed response.&lt;/p&gt;

&lt;p&gt;Let’s fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Streaming on the Client
&lt;/h2&gt;

&lt;p&gt;Even when dealing with streams, the default browser behavior when receiving a response is to wait for it to complete. In order to get the behavior we want, we’ll need to use client-side JavaScript to make the request and process the streaming body of the response.&lt;/p&gt;

&lt;p&gt;We’ve already tackled that first part inside our &lt;code&gt;handleSubmit&lt;/code&gt; function. Let’s start processing that response body.&lt;/p&gt;

&lt;p&gt;We can access the &lt;code&gt;ReadableStream&lt;/code&gt; from the response body’s &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader" rel="noopener noreferrer"&gt;&lt;code&gt;getReader()&lt;/code&gt;&lt;/a&gt; method. This stream will have its own &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read" rel="noopener noreferrer"&gt;&lt;code&gt;read()&lt;/code&gt;&lt;/a&gt; method that we can use to access the next chunk of data, as well as the information if the response is done streaming or not.&lt;/p&gt;

&lt;p&gt;The only ‘gotcha’ is that the data in each chunk doesn’t come in as text, it comes in as a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array" rel="noopener noreferrer"&gt;&lt;code&gt;Uint8Array&lt;/code&gt;&lt;/a&gt;, which is “an array of 8-bit unsigned integers.” It’s basically the representation of the binary data, and you don’t really need to understand any deeper than that unless you want to sound very smart at a party (or boring).&lt;/p&gt;

&lt;p&gt;The important thing to understand is that on their own, these data chunks aren’t very useful. To get something we &lt;strong&gt;can&lt;/strong&gt; use, we’ll need to decode each chunk of data using a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder" rel="noopener noreferrer"&gt;&lt;code&gt;TextDecoder&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ok, that’s a lot of theory. Let’s break down the logic and then look at some code.&lt;/p&gt;

&lt;p&gt;When we get the response back, we need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grab the reader from the response body using&lt;/strong&gt; &lt;code&gt;response.body.getReader()&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Setup a decoder using&lt;/strong&gt; &lt;code&gt;TextDecoder&lt;/code&gt; and a variable to track the streaming status.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Process each chunk until the stream is complete, with a&lt;/strong&gt; &lt;code&gt;while&lt;/code&gt; loop that does this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Grab the next chunk’s data and stream status.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decode the data and use it to update our app’s&lt;/strong&gt; &lt;code&gt;state.text&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Update the streaming status variable, terminating the loop when complete.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update the loading state of the app by setting&lt;/strong&gt; &lt;code&gt;state.isLoading&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;

&lt;p&gt;The new &lt;code&gt;handleSubmit&lt;/code&gt; function should look something like this:&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;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="cm"&gt;/** @type {HTMLFormElement} */&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Parse streaming body&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&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;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isStillStreaming&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;}&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;chunkValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunkValue&lt;/span&gt;

    &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&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="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when I submit the form, I see something like:&lt;/p&gt;

&lt;p&gt;“Ground  &lt;/p&gt;

&lt;p&gt;control  &lt;/p&gt;

&lt;p&gt;to major  &lt;/p&gt;

&lt;p&gt;Tom.”&lt;/p&gt;

&lt;p&gt;Hell yeah!!!&lt;/p&gt;

&lt;p&gt;OK, most of the work is down. Now we just need to replace our demo stream with the OpenAI response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stream OpenAI Response
&lt;/h2&gt;

&lt;p&gt;Looking back at our original implementation, the first thing we need to do is modify the request to OpenAI to let them know that we would like a streaming response. We can do that by setting the &lt;a href="https://platform.openai.com/docs/api-reference/chat/create#chat/create-stream" rel="noopener noreferrer"&gt;&lt;code&gt;stream&lt;/code&gt;&lt;/a&gt; property in the &lt;code&gt;fetch&lt;/code&gt; payload to &lt;code&gt;true&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&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="nf"&gt;stringify&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;UPDATE 2023/11/15:&lt;/strong&gt; I used fetch and custom streams because at the time of writing, the &lt;a href="https://www.npmjs.com/package/openai" rel="noopener noreferrer"&gt;&lt;code&gt;openai&lt;/code&gt;&lt;/a&gt; module on NPM did not properly support streaming responses. That issue has been fixed, and I think a better solution would be to use that module and pipe their data through a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/TransformStream" rel="noopener noreferrer"&gt;&lt;code&gt;TransformStream&lt;/code&gt;&lt;/a&gt; to send to the client. That version is not reflected here.&lt;/p&gt;

&lt;p&gt;Next, we could pipe the response from OpenAI directly to the client, but we might not want to do that. The data they send doesn’t really align with that we want to send to the client because it looks like this (two chunks, one with data, and one representing the end of the stream):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&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;id&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;chatcmpl-4bJZRnslkje3289REHFEH9ej2&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;object&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;chat.completion.chunk&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;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1690319476&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&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;gpt-3.5-turbo-0613&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;choiced&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;index&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delta&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;content&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;Because&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;finish_reason&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;stop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DONE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, what we’ll do is create our own stream, similar to the David Bowie lyrics, that will do some setup, enqueue chunks of data into the stream, and close the stream. Let’s start with an outline:&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Any setup before streaming   &lt;/span&gt;
    &lt;span class="c1"&gt;// Send chunks of data&lt;/span&gt;
    &lt;span class="c1"&gt;// Close stream&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;Since we’re dealing with a streaming fetch response from OpenAI, a lot of the work we need to do here can actually be copied from the client-side stream handling. This part should look familiar:&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&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;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isStillStreaming&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;}&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;chunkValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Here's where things will be different&lt;/span&gt;

  &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snippet was taken almost directly from the frontend stream processing example. The only difference is that we need to treat the data coming from OpenAI slightly differently. As we say, the chunks of data they send up will look something like”&lt;code&gt;data: [JSON data or done]&lt;/code&gt;“. Another gotcha is that every once in a while, they’ll actually slip in TWO of these data strings in a single streaming chunk. So here’s what I came up with for processing the data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a&lt;/strong&gt; &lt;a href="https://regex101.com/r/R4QgmZ/1" rel="noopener noreferrer"&gt;&lt;strong&gt;Regular Expression&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;to grab the rest of the string after “&lt;/strong&gt;&lt;code&gt;data:&lt;/code&gt; “.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For the unlikely event there are more than one data strings, use a while loop to process every match in the string.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If the current matches the closing condition (“&lt;/strong&gt;&lt;code&gt;[DONE]&lt;/code&gt;“) close the stream.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Otherwise, parse the data as JSON and enqueue the first piece of text from the list of options (&lt;/strong&gt;&lt;code&gt;json.choices[0].delta.content&lt;/code&gt;). Fall back to an empty string if none is present.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lastly, in order to move to the next match, if there is one, we can use&lt;/strong&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec" rel="noopener noreferrer"&gt;&lt;code&gt;RegExp.exec()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The logic is quite abstract without looking at code, so here’s what the whole stream looks like now:&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Do work before streaming&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&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;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isStillStreaming&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;}&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;chunkValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="cm"&gt;/**
       * Captures any string after the text `data: `
       * @see https://regex101.com/r/R4QgmZ/1
       */&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/data:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&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="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&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;// Close stream&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[DONE]&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;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="k"&gt;break&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="k"&gt;try&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;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;

            &lt;span class="c1"&gt;// Send chunk of data&lt;/span&gt;
            &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkValue&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextChunk&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;nextChunkValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextChunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkValue&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;nextChunkValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;UPDATE 2023/11/15:&lt;/strong&gt; I discovered that OpenAI API sometimes returns the JSON payload across two streams. So the solution is to use a &lt;code&gt;try/catch&lt;/code&gt; block around the &lt;code&gt;JSON.parse&lt;/code&gt; and in the case that it fails, reassign the &lt;code&gt;match&lt;/code&gt; variable to the current chunk value plus the next chunk value. The code above has the updated snippet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review
&lt;/h2&gt;

&lt;p&gt;That should be everything we need to get streaming working. Hopefully it all makes sense and you got it working on your end.&lt;/p&gt;

&lt;p&gt;I think it’s a good idea to review the flow to make sure we’ve got it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The user submits the form, which gets intercepted and sent with JavaScript. This is necessary to process the stream when it returns.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The request is received by the action handler which forwards the data to the OpenAI API along with the setting to return the response as a stream.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The OpenAI response will be sent back as a stream of chunks, some of which contain JSON and the last one being “&lt;/strong&gt;&lt;code&gt;[DONE]&lt;/code&gt;“.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instead of passing the stream to the action response, we create a new stream to use in the response.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inside this stream, we process each chunk of data from the OpenAI response and convert it to something more useful before enqueuing it for the action response stream.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When the OpenAI stream closes, we also close our action stream.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The JavaScript handler on the client side will also process each chunk of data as it comes in and update the UI accordingly.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The app is working. It’s pretty cool. We covered a lot of interesting things today. Streams are very powerful, but also challenging and, especially when working within Qwik, there are a couple of little gotchas. But because we focused on low-level fundamentals, these concepts should apply across any framework.&lt;/p&gt;

&lt;p&gt;As long as you have access to the platform and primitives like streams, requests, and response objects then this should work. That’s the beauty of fundamentals.&lt;/p&gt;

&lt;p&gt;I think we got a pretty decent application going now. The only problem is right now we’re using a generic text input and asking users to fill in the entire prompt themselves. In fact, they can put in whatever they want. We’ll want to fix that in a future post, but the next post is going to step away from code and focus on understanding how the AI tools actually work.&lt;/p&gt;

&lt;p&gt;I hope you’ve been enjoying this series and come back for the rest of it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/" rel="noopener noreferrer"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/" rel="noopener noreferrer"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/" rel="noopener noreferrer"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/" rel="noopener noreferrer"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/" rel="noopener noreferrer"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil" rel="noopener noreferrer"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil" rel="noopener noreferrer"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AI for Web Devs: Faster Responses with HTTP Streaming</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Tue, 16 Jan 2024 19:12:20 +0000</pubDate>
      <link>https://dev.to/austingil/ai-for-web-devs-faster-responses-with-http-streaming-3ipe</link>
      <guid>https://dev.to/austingil/ai-for-web-devs-faster-responses-with-http-streaming-3ipe</guid>
      <description>&lt;p&gt;Welcome back to &lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;this series&lt;/a&gt; where we’re building web applications with AI tooling.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In &lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;the previous post&lt;/a&gt;, we got &lt;a href="https://austingil.com/category/ai/"&gt;AI&lt;/a&gt; generated jokes into our &lt;a href="https://qwik.builder.io/"&gt;Qwik&lt;/a&gt; application from &lt;a href="https://platform.openai.com/"&gt;OpenAI API&lt;/a&gt;. It worked, but the user experience suffered because we had to wait until the API completed the entire response before updating the client.&lt;/p&gt;

&lt;p&gt;A better experience, as you’ll know if you’ve used any AI chat tools, is to respond as soon as each bit of text is generated. It becomes a sort of teletype effect.&lt;/p&gt;

&lt;p&gt;That’s what we’re going to build today using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Streams_API"&gt;HTTP streams&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/GkyHBwUA0EQ"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we get into streams, we need to explore something with a Qwik quirk related to HTTP requests.&lt;/p&gt;

&lt;p&gt;If we examine the current POST request being sent by the form, we can see that the returned payload isn’t just the plain text we returned from our action handler. Instead, it’s this sort of &lt;a href="https://qwik.builder.io/docs/guides/serialization/#serialization-boundary"&gt;serialized data&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--BK5G6TVJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/image-92-1080x612.png%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--BK5G6TVJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.statically.io/img/austingil.com/wp-content/uploads/image-92-1080x612.png%3Fquality%3D100%26f%3Dauto%2520align%3D%2522left%2522" alt="Screenshot of Chrome devtools Network tab with a serialized reponse from the Qwik backend" width="800" height="453"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is the result of how the &lt;a href="https://qwik.builder.io/docs/advanced/optimizer/"&gt;Qwik Optimizer&lt;/a&gt; lazy loads assets and is necessary to properly handle the data as it comes back. Unfortunately, this prevents standard streaming responses.&lt;/p&gt;

&lt;p&gt;So while &lt;a href="https://qwik.builder.io/docs/action/#routeaction"&gt;&lt;code&gt;routeAction$&lt;/code&gt;&lt;/a&gt; and the &lt;code&gt;Form&lt;/code&gt; component are super handy, we’ll have to do something else.&lt;/p&gt;

&lt;p&gt;To their credit, the Qwik team does provide a &lt;a href="https://qwik.builder.io/docs/server$/#streaming-responses"&gt;well-documented approach for streaming responses&lt;/a&gt;. However, it involves their &lt;code&gt;server$&lt;/code&gt; function and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncGenerator"&gt;async generator functions&lt;/a&gt;. This would probably be the right approach if we’re talking strictly about Qwik, but this series is for everyone. I’ll avoid this implementation, as it’s too specific to Qwik, and focus on broadly applicable concepts instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Refactor Server Logic
&lt;/h2&gt;

&lt;p&gt;It sucks that we can’t use route actions because they’re great. So what can we use?&lt;/p&gt;

&lt;p&gt;Qwik City offers a few options. The best I found is &lt;a href="https://qwik.builder.io/docs/middleware/#middleware"&gt;middleware&lt;/a&gt;. They provide enough access to primitive tools that we can accomplish what we need, and the concepts will apply to other contexts besides Qwik.&lt;/p&gt;

&lt;p&gt;Middleware is essentially a set of functions that we can inject at various points within the request lifecycle of our route handler. We can define them by exporting named constants for the hooks we want to target (&lt;code&gt;onRequest&lt;/code&gt;, &lt;code&gt;onGet&lt;/code&gt;, &lt;code&gt;onPost&lt;/code&gt;, &lt;code&gt;onPut&lt;/code&gt;, &lt;code&gt;onDelete&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;So instead of relying on a route action, we can use a middleware that hooks into any POST request by exporting an &lt;code&gt;onPost&lt;/code&gt; middleware. In order to support streaming, we’ll want to return a standard &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Response"&gt;Response&lt;/a&gt; object. We can do so by creating a Response object and passing it to the &lt;a href="https://qwik.builder.io/docs/middleware/#send"&gt;&lt;code&gt;requestEvent.send()&lt;/code&gt;&lt;/a&gt; method.&lt;/p&gt;

&lt;p&gt;Here’s a basic (non-streaming) example:&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="cm"&gt;/** @type {import('@builder.io/qwik-city').RequestHandler} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello Squirrel!&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;Before we tackle streaming, let’s get the same functionality from the old route action implemented with middleware. We can copy most of the code into the &lt;code&gt;onPost&lt;/code&gt; middleware, but we won’t have access to &lt;code&gt;formData&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fortunately, we can recreate that data from the &lt;a href="https://qwik.builder.io/docs/middleware/#parsebody"&gt;&lt;code&gt;requestEvent.parseBody()&lt;/code&gt;&lt;/a&gt; method. We’ll also want to use &lt;code&gt;requestEvent.send()&lt;/code&gt; to respond with the OpenAI data instead of a &lt;code&gt;return&lt;/code&gt; statement.&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="cm"&gt;/** @type {import('@builder.io/qwik-city').RequestHandler} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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;OPENAI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="dl"&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;formData&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;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parseBody&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="c1"&gt;// ... fetch options&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="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="nf"&gt;json&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;responseBody&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="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;

  &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;responseBody&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;
  
  
  Refactor Client Logic
&lt;/h2&gt;

&lt;p&gt;Replacing the route actions has the unfortunate side effect of meaning we also can’t use the &lt;code&gt;&amp;lt;Form&amp;gt;&lt;/code&gt; component anymore. We’ll have to use a regular &lt;a href="https://austingil.com/category/html/"&gt;HTML&lt;/a&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form"&gt;&lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;&lt;/a&gt; element and recreate all the benefits we had before, including sending HTTP request with &lt;a href="https://austingil.com/category/javascript/"&gt;JavaScript&lt;/a&gt;, tracking the loading state, and accessing the results. Let’s refactor our client-side to support those features again.&lt;/p&gt;

&lt;p&gt;We can break these requirements down to needing two things, a JavaScript solution for submitting forms and a reactive state for managing loading states and results.&lt;/p&gt;

&lt;p&gt;I’ve covered submitting HTML forms with JavaScript in depth several times in the past:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/resilient-applications-progressive-enhancement/"&gt;&lt;strong&gt;Make Beautifully Resilient Apps With Progressive Enhancement&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/upload-files-with-javascript/"&gt;&lt;strong&gt;How to Upload Files with JavaScript&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=ATqDrIkA8fA&amp;amp;t=1s"&gt;&lt;strong&gt;Building Super Powered HTML Forms with JavaScript&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So today I’ll just share the snippet, which I put inside a &lt;code&gt;utils.js&lt;/code&gt; file in the root of my project. This &lt;code&gt;jsFormSubmit&lt;/code&gt; function accepts an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement"&gt;&lt;code&gt;HTMLFormElement&lt;/code&gt;&lt;/a&gt; then constructs a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API"&gt;&lt;code&gt;fetch&lt;/code&gt;&lt;/a&gt; request based on the form attributes and returns the resulting &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise"&gt;Promise&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="cm"&gt;/**
 * @param {HTMLFormElement} form 
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&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;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&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;searchParameters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URLSearchParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="cm"&gt;/** @type {Parameters&amp;lt;typeof fetch&amp;gt;[1]} */&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fetchOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&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;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&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;post&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;fetchOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enctype&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multipart/form-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;searchParameters&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;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParameters&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fetchOptions&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;This generic function can be used to submit any HTML form, so it’s handy to use in a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLFormElement/submit_event"&gt;&lt;code&gt;submit&lt;/code&gt; event&lt;/a&gt; handler. Sweet!&lt;/p&gt;

&lt;p&gt;As for the reactive data, Qwik provides two options, &lt;a href="https://qwik.builder.io/docs/components/state/#usestore"&gt;&lt;code&gt;useStore&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://qwik.builder.io/docs/components/state/#usesignal"&gt;&lt;code&gt;useSignal&lt;/code&gt;&lt;/a&gt;. I prefer &lt;code&gt;useStore&lt;/code&gt;, which allows us to create an object whose properties are &lt;a href="https://qwik.builder.io/docs/concepts/reactivity/"&gt;reactive&lt;/a&gt;. Meaning changes to the object’s properties will automatically be reflected wherever they are referenced in the UI.&lt;/p&gt;

&lt;p&gt;We can use &lt;code&gt;useStore&lt;/code&gt; to create a “state” object in our component to track the loading state of the HTTP request as well as the text response.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useStore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// other setup logic&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&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;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useStore&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;isLoading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&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="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// other component logic&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we can update the template. Since we can no longer use the &lt;code&gt;action&lt;/code&gt; object we had before, we can replace references from &lt;code&gt;action.isRunning&lt;/code&gt; and &lt;code&gt;action.value&lt;/code&gt; to &lt;code&gt;state.isLoading&lt;/code&gt; and &lt;code&gt;state.text&lt;/code&gt;, respectively (don’t ask me why I changed the property names 🤷‍♂️). I’ll also add a “submit” event handler to the form called &lt;code&gt;handleSbumit&lt;/code&gt;, which we’ll look at shortly.&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; 
    &lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;preventdefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;
    &lt;span class="nx"&gt;onSubmit$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;handleSubmit&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Prompt&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/label&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;Tell&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;joke&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/textarea&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&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="nx"&gt;isLoading&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;One sec...&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;Tell me&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
  &lt;span class="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="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="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="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/article&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/main&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that the &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; does not explicitly provide an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#action"&gt;&lt;code&gt;action&lt;/code&gt;&lt;/a&gt; attribute. By default, an HTML form will submit data to the current URL, so we only need to set the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form#method"&gt;&lt;code&gt;method&lt;/code&gt;&lt;/a&gt; to POST and submit this form to trigger the &lt;code&gt;onPost&lt;/code&gt; middleware we defined earlier.&lt;/p&gt;

&lt;p&gt;Now, the last step to get this refactor working is defining &lt;code&gt;handleSubmit&lt;/code&gt;. Just like we did in &lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;the previous post&lt;/a&gt;, we need to wrap an event handler inside Qwik’s &lt;code&gt;$&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;Inside the event handler, we’ll want to clear out any previous data from &lt;code&gt;state.text&lt;/code&gt;, set &lt;code&gt;state.isLoading&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, then pass the form’s DOM node to our fancy &lt;code&gt;jsFormSubmit&lt;/code&gt; function. This should submit the HTTP request for us. Once it comes back, we can update &lt;code&gt;state.text&lt;/code&gt; with the response body, and return &lt;code&gt;state.isLoading&lt;/code&gt; to &lt;code&gt;false&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="cm"&gt;/** @type {HTMLFormElement} */&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&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="nx"&gt;text&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="nf"&gt;text&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="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OK! We should now have a client-side form that uses JavaScript to submit an HTTP request to the server while tracking the loading and response states, and updating the UI accordingly.&lt;/p&gt;

&lt;p&gt;That was a lot of work to get the same solution we had before but with fewer features. BUT the key benefit is we now have direct access to the platform primitives we need to support streaming.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Streaming on the Server
&lt;/h2&gt;

&lt;p&gt;Before we start streaming responses from OpenAI, I think it’s helpful to start with a very basic example to get a better grasp of streams. Streams allow us to send small chunks of data over time. So as an example, let’s print out some iconic David Bowie lyrics in tempo with the song, “&lt;a href="https://www.youtube.com/watch?v=iYYRH4apXDo"&gt;Space Oddity&lt;/a&gt;“.&lt;/p&gt;

&lt;p&gt;When we construct our Response object, instead of passing plain text, we’ll want to pass a stream. We’ll create the stream shortly, but here’s the idea:&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="cm"&gt;/** @type {import('@builder.io/qwik-city').RequestHandler} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&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 create a very rudimentary &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream"&gt;&lt;code&gt;ReadableStream&lt;/code&gt;&lt;/a&gt; using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream"&gt;&lt;code&gt;ReadableStream&lt;/code&gt; constructor&lt;/a&gt; and pass it an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream#parameters"&gt;optional parameter&lt;/a&gt;. This optional parameter can be an object with a &lt;code&gt;start&lt;/code&gt; method that’s called when the stream is constructed.&lt;/p&gt;

&lt;p&gt;The start method is responsible for the steam’s logic and has access to the stream &lt;code&gt;controller&lt;/code&gt;, which is used to send data and close the stream.&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Stream logic goes here&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;OK, let’s plan out that logic. We’ll have an array of song lyrics and a function to ‘sing’ them (pass them to the stream). The &lt;code&gt;sing&lt;/code&gt; function will take the first item in the array and pass that to the stream using the &lt;code&gt;controller.enqueue()&lt;/code&gt; method. If it’s the last lyric in the list, we can close the stream with &lt;code&gt;controller.close()&lt;/code&gt;. Otherwise, the &lt;code&gt;sing&lt;/code&gt; method can call itself again after a short pause.&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&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;lyrics&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;Ground&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; control&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; to major&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; Tom.&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="nf"&gt;sing&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;lyric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lyrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

      &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lyric&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;lyrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&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="nf"&gt;sing&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;So each second, for four seconds, this stream will send out the lyrics “Ground control to major Tom.” Slick!&lt;/p&gt;

&lt;p&gt;Because this stream will be used in the body of the Response, the connection will remain open for four seconds until the response completes. But the frontend will have access to each chunk of data as it arrives, rather than waiting the full four seconds.&lt;/p&gt;

&lt;p&gt;This doesn’t speed up the total response time (in some cases, streams can increase response times), but it does allow for a faster &lt;strong&gt;perceived&lt;/strong&gt; response, and that makes a better user experience.&lt;/p&gt;

&lt;p&gt;Here’s what my code looks like:&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="cm"&gt;/** @type {import('@builder.io/qwik-city').RequestHandler} */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onPost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RequestHandler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requestEvent&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&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;lyrics&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;Ground&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; control&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; to major&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; Tom.&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="nf"&gt;sing&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;lyric&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lyrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lyric&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;lyrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&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="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&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="nf"&gt;sing&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;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&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;Unfortunately, as it stands right now, the client will still be waiting four seconds before seeing the entire response, and that’s because we weren’t expecting a streamed response.&lt;/p&gt;

&lt;p&gt;Let’s fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Streaming on the Client
&lt;/h2&gt;

&lt;p&gt;Even when dealing with streams, the default browser behavior when receiving a response is to wait for it to complete. In order to get the behavior we want, we’ll need to use client-side JavaScript to make the request and process the streaming body of the response.&lt;/p&gt;

&lt;p&gt;We’ve already tackled that first part inside our &lt;code&gt;handleSubmit&lt;/code&gt; function. Let’s start processing that response body.&lt;/p&gt;

&lt;p&gt;We can access the &lt;code&gt;ReadableStream&lt;/code&gt; from the response body’s &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/getReader"&gt;&lt;code&gt;getReader()&lt;/code&gt;&lt;/a&gt; method. This stream will have its own &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader/read"&gt;&lt;code&gt;read()&lt;/code&gt;&lt;/a&gt; method that we can use to access the next chunk of data, as well as the information if the response is done streaming or not.&lt;/p&gt;

&lt;p&gt;The only ‘gotcha’ is that the data in each chunk doesn’t come in as text, it comes in as a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array"&gt;&lt;code&gt;Uint8Array&lt;/code&gt;&lt;/a&gt;, which is “an array of 8-bit unsigned integers.” It’s basically the representation of the binary data, and you don’t really need to understand any deeper than that unless you want to sound very smart at a party (or boring).&lt;/p&gt;

&lt;p&gt;The important thing to understand is that on their own, these data chunks aren’t very useful. To get something we &lt;strong&gt;can&lt;/strong&gt; use, we’ll need to decode each chunk of data using a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder"&gt;&lt;code&gt;TextDecoder&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Ok, that’s a lot of theory. Let’s break down the logic and then look at some code.&lt;/p&gt;

&lt;p&gt;When we get the response back, we need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Grab the reader from the response body using&lt;/strong&gt; &lt;code&gt;response.body.getReader()&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Setup a decoder using&lt;/strong&gt; &lt;code&gt;TextDecoder&lt;/code&gt; and a variable to track the streaming status.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Process each chunk until the stream is complete, with a&lt;/strong&gt; &lt;code&gt;while&lt;/code&gt; loop that does this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Grab the next chunk’s data and stream status.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decode the data and use it to update our app’s&lt;/strong&gt; &lt;code&gt;state.text&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Update the streaming status variable, terminating the loop when complete.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update the loading state of the app by setting&lt;/strong&gt; &lt;code&gt;state.isLoading&lt;/code&gt; to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The new &lt;code&gt;handleSubmit&lt;/code&gt; function should look something like this:&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;handleSubmit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="cm"&gt;/** @type {HTMLFormElement} */&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&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="nf"&gt;jsFormSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Parse streaming body&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&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;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

  &lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isStillStreaming&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;}&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;chunkValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;chunkValue&lt;/span&gt;

    &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&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="nx"&gt;isLoading&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when I submit the form, I see something like:&lt;/p&gt;

&lt;p&gt;“Ground  &lt;/p&gt;

&lt;p&gt;control  &lt;/p&gt;

&lt;p&gt;to major  &lt;/p&gt;

&lt;p&gt;Tom.”&lt;/p&gt;

&lt;p&gt;Hell yeah!!!&lt;/p&gt;

&lt;p&gt;OK, most of the work is down. Now we just need to replace our demo stream with the OpenAI response.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stream OpenAI Response
&lt;/h2&gt;

&lt;p&gt;Looking back at our original implementation, the first thing we need to do is modify the request to OpenAI to let them know that we would like a streaming response. We can do that by setting the &lt;a href="https://platform.openai.com/docs/api-reference/chat/create#chat/create-stream"&gt;&lt;code&gt;stream&lt;/code&gt;&lt;/a&gt; property in the &lt;code&gt;fetch&lt;/code&gt; payload to &lt;code&gt;true&lt;/code&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&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="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&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="nf"&gt;stringify&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;UPDATE 2023/11/15:&lt;/strong&gt; I used fetch and custom streams because at the time of writing, the &lt;a href="https://www.npmjs.com/package/openai"&gt;&lt;code&gt;openai&lt;/code&gt;&lt;/a&gt; module on NPM did not properly support streaming responses. That issue has been fixed, and I think a better solution would be to use that module and pipe their data through a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/TransformStream"&gt;&lt;code&gt;TransformStream&lt;/code&gt;&lt;/a&gt; to send to the client. That version is not reflected here.&lt;/p&gt;

&lt;p&gt;Next, we could pipe the response from OpenAI directly to the client, but we might not want to do that. The data they send doesn’t really align with that we want to send to the client because it looks like this (two chunks, one with data, and one representing the end of the stream):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&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;id&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;chatcmpl-4bJZRnslkje3289REHFEH9ej2&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;object&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;chat.completion.chunk&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;created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1690319476&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&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;gpt-3.5-turbo-0613&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;choiced&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;index&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;delta&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;content&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;Because&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;finish_reason&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;stop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;DONE&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, what we’ll do is create our own stream, similar to the David Bowie lyrics, that will do some setup, enqueue chunks of data into the stream, and close the stream. Let’s start with an outline:&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Any setup before streaming   &lt;/span&gt;
    &lt;span class="c1"&gt;// Send chunks of data&lt;/span&gt;
    &lt;span class="c1"&gt;// Close stream&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;Since we’re dealing with a streaming fetch response from OpenAI, a lot of the work we need to do here can actually be copied from the client-side stream handling. This part should look familiar:&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;reader&lt;/span&gt; &lt;span class="o"&gt;=&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&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;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isStillStreaming&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;}&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;chunkValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Here's where things will be different&lt;/span&gt;

  &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This snippet was taken almost directly from the frontend stream processing example. The only difference is that we need to treat the data coming from OpenAI slightly differently. As we say, the chunks of data they send up will look something like”&lt;code&gt;data: [JSON data or done]&lt;/code&gt;“. Another gotcha is that every once in a while, they’ll actually slip in TWO of these data strings in a single streaming chunk. So here’s what I came up with for processing the data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a&lt;/strong&gt; &lt;a href="https://regex101.com/r/R4QgmZ/1"&gt;&lt;strong&gt;Regular Expression&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;to grab the rest of the string after “&lt;/strong&gt;&lt;code&gt;data:&lt;/code&gt; “.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;For the unlikely event there are more than one data strings, use a while loop to process every match in the string.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If the current matches the closing condition (“&lt;/strong&gt;&lt;code&gt;[DONE]&lt;/code&gt;“) close the stream.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Otherwise, parse the data as JSON and enqueue the first piece of text from the list of options (&lt;/strong&gt;&lt;code&gt;json.choices[0].delta.content&lt;/code&gt;). Fall back to an empty string if none is present.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Lastly, in order to move to the next match, if there is one, we can use&lt;/strong&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec"&gt;&lt;code&gt;RegExp.exec()&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The logic is quite abstract without looking at code, so here’s what the whole stream looks like now:&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ReadableStream&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Do work before streaming&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&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;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getReader&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;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isStillStreaming&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;done&lt;/span&gt;&lt;span class="p"&gt;}&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;chunkValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="cm"&gt;/**
       * Captures any string after the text `data: `
       * @see https://regex101.com/r/R4QgmZ/1
       */&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/data:&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;.*&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&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="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&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;// Close stream&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[DONE]&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;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="k"&gt;break&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="k"&gt;try&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;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;delta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;

            &lt;span class="c1"&gt;// Send chunk of data&lt;/span&gt;
            &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enqueue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkValue&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextChunk&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;reader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;nextChunkValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextChunk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;regex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;chunkValue&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;nextChunkValue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;isStillStreaming&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;done&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;UPDATE 2023/11/15:&lt;/strong&gt; I discovered that OpenAI API sometimes returns the JSON payload across two streams. So the solution is to use a &lt;code&gt;try/catch&lt;/code&gt; block around the &lt;code&gt;JSON.parse&lt;/code&gt; and in the case that it fails, reassign the &lt;code&gt;match&lt;/code&gt; variable to the current chunk value plus the next chunk value. The code above has the updated snippet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review
&lt;/h2&gt;

&lt;p&gt;That should be everything we need to get streaming working. Hopefully it all makes sense and you got it working on your end.&lt;/p&gt;

&lt;p&gt;I think it’s a good idea to review the flow to make sure we’ve got it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The user submits the form, which gets intercepted and sent with JavaScript. This is necessary to process the stream when it returns.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The request is received by the action handler which forwards the data to the OpenAI API along with the setting to return the response as a stream.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The OpenAI response will be sent back as a stream of chunks, some of which contain JSON and the last one being “&lt;/strong&gt;&lt;code&gt;[DONE]&lt;/code&gt;“.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instead of passing the stream to the action response, we create a new stream to use in the response.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inside this stream, we process each chunk of data from the OpenAI response and convert it to something more useful before enqueuing it for the action response stream.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When the OpenAI stream closes, we also close our action stream.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The JavaScript handler on the client side will also process each chunk of data as it comes in and update the UI accordingly.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The app is working. It’s pretty cool. We covered a lot of interesting things today. Streams are very powerful, but also challenging and, especially when working within Qwik, there are a couple of little gotchas. But because we focused on low-level fundamentals, these concepts should apply across any framework.&lt;/p&gt;

&lt;p&gt;As long as you have access to the platform and primitives like streams, requests, and response objects then this should work. That’s the beauty of fundamentals.&lt;/p&gt;

&lt;p&gt;I think we got a pretty decent application going now. The only problem is right now we’re using a generic text input and asking users to fill in the entire prompt themselves. In fact, they can put in whatever they want. We’ll want to fix that in a future post, but the next post is going to step away from code and focus on understanding how the AI tools actually work.&lt;/p&gt;

&lt;p&gt;I hope you’ve been enjoying this series and come back for the rest of it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/ai-for-web-devs-set-up/"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>AI for Web Devs: Your First API Request to OpenAI</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Tue, 16 Jan 2024 17:40:24 +0000</pubDate>
      <link>https://dev.to/austingil/ai-for-web-devs-your-first-api-request-to-openai-5boe</link>
      <guid>https://dev.to/austingil/ai-for-web-devs-your-first-api-request-to-openai-5boe</guid>
      <description>&lt;p&gt;Welcome back to this series where we are learning how to integrate AI products into web applications.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/" rel="noopener noreferrer"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/" rel="noopener noreferrer"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/" rel="noopener noreferrer"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/" rel="noopener noreferrer"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/" rel="noopener noreferrer"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;Last time&lt;/a&gt;, we got all the boilerplate work out of the way.&lt;/p&gt;

&lt;p&gt;In this post, we’ll learn how to integrate OpenAI’s API responses into our Qwik app using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API" rel="noopener noreferrer"&gt;&lt;code&gt;fetch&lt;/code&gt;&lt;/a&gt;. We’ll want to make sure we’re not leaking API keys by executing these HTTP requests from a backend.&lt;/p&gt;

&lt;p&gt;By the end of this post, we will have a rudimentary, but &lt;strong&gt;working&lt;/strong&gt; AI application.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/gUgRD0sRoCU"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate OpenAI API Key
&lt;/h2&gt;

&lt;p&gt;Before we start building anything, you’ll need to go to &lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;platform.openai.com/account/api-keys&lt;/a&gt; and generate an API key to use in your application.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-90-1080x551.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-90-1080x551.png%3Fquality%3D100%26f%3Dauto%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make sure to keep a copy of it somewhere because you will only be able to see it once.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With your API key, you’ll be able to make authenticated HTTP requests to &lt;a href="https://openai.com/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;. So it’s a good idea to get familiar with the API itself. I’d encourage you to take a brief look through the &lt;a href="https://platform.openai.com/docs/introduction" rel="noopener noreferrer"&gt;OpenAI Documentation&lt;/a&gt; and become familiar with some concepts. The &lt;a href="https://platform.openai.com/docs/models" rel="noopener noreferrer"&gt;models&lt;/a&gt; are particularly good to understand because they have varying capabilities.&lt;/p&gt;

&lt;p&gt;If you would like to familiarize yourself with the API endpoints, expected payloads, and return values, check out the &lt;a href="https://platform.openai.com/docs/api-reference" rel="noopener noreferrer"&gt;OpenAI API Reference&lt;/a&gt;. It also contains helpful examples.&lt;/p&gt;

&lt;p&gt;You may notice the &lt;a href="https://austingil.com/category/javascript/" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; package available on NPM called &lt;a href="https://www.npmjs.com/package/openai" rel="noopener noreferrer"&gt;&lt;code&gt;openai&lt;/code&gt;&lt;/a&gt;. We will &lt;em&gt;not&lt;/em&gt; be using this, as it doesn’t quite support some things we’ll want to do, that &lt;code&gt;fetch&lt;/code&gt; can.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make Your First HTTP Request
&lt;/h2&gt;

&lt;p&gt;The application we’re going to build will make an AI-generated text completion based on the user input. For that, we’ll want to work with the &lt;a href="https://platform.openai.com/docs/api-reference/chat/create" rel="noopener noreferrer"&gt;chat endpoint&lt;/a&gt; (note that the completions endpoint is deprecated).&lt;/p&gt;

&lt;p&gt;We need to make a &lt;code&gt;POST&lt;/code&gt; request to &lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;https://api.openai.com/v1/chat/completions&lt;/a&gt; with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type" rel="noopener noreferrer"&gt;&lt;code&gt;'Content-Type'&lt;/code&gt;&lt;/a&gt; header set to &lt;code&gt;'application/json'&lt;/code&gt;, the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization" rel="noopener noreferrer"&gt;&lt;code&gt;'Authorization'&lt;/code&gt;&lt;/a&gt; set to &lt;code&gt;'Bearer OPENAI_API_KEY'&lt;/code&gt; (you’ll need to replace OPENAI_API_KEY with your API key), and the &lt;code&gt;body&lt;/code&gt; set to a JSON string containing the GPT model to use (we’ll use &lt;a href="https://platform.openai.com/docs/models/gpt-3-5" rel="noopener noreferrer"&gt;&lt;code&gt;gpt-3.5-turbo&lt;/code&gt;&lt;/a&gt;) and an array of messages:&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="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&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;Authorization&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;Bearer OPENAI_API_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;body&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="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;model&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;gpt-3.5-turbo&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;messages&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&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;user&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;content&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;Tell me a funny joke&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;p&gt;You can run this right from your browser console and see the request in the Network tab of your dev tools.&lt;/p&gt;

&lt;p&gt;The response should be a JSON object with a bunch of properties, but the one we’re most interested in is the &lt;code&gt;"choices"&lt;/code&gt;. It will be an array of text completions objects. The first one should be an object with a &lt;code&gt;"message"&lt;/code&gt; object that has a &lt;code&gt;"content"&lt;/code&gt; property with the chat completion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chatcmpl-7q63Hd9pCPxY3H4pW67f1BPSmJs2u"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"chat.completion"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1692650675&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-3.5-turbo-0613"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Why don't scientists trust atoms?&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Because they make up everything!"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"finish_reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"stop"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"usage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prompt_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"completion_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Congrats! Now you can request a mediocre joke whenever you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the Form
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;fetch&lt;/code&gt; request above is fine, but it’s not quite an application. What we want is something a user can interact with to generate an HTTP request like the one above.&lt;/p&gt;

&lt;p&gt;For that, we’ll probably want some sort to start with an &lt;a href="https://austingil.com/category/html/" rel="noopener noreferrer"&gt;HTML&lt;/a&gt; &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt;&lt;/a&gt; containing a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/textarea" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt;&lt;/a&gt;. Below is the minimum markup we need, and if you want to learn more, consider reading these articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/how-to-build-html-forms-right-semantics/" rel="noopener noreferrer"&gt;&lt;strong&gt;“How to Build HTML Forms Right: Semantics”&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/how-to-build-html-forms-right-accessibility/" rel="noopener noreferrer"&gt;&lt;strong&gt;“How to Build HTML Forms Right: Accessibility”&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.freecodecamp.org/news/perfect-html-input/" rel="noopener noreferrer"&gt;&lt;strong&gt;“How to Build Great HTML Form Controls”&lt;/strong&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&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;"prompt"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Prompt&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;textarea&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"prompt"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"prompt"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/textarea&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;button&amp;gt;&lt;/span&gt;Tell me&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can copy and paste this form right inside our Qwik component’s JSX template. If you’ve worked with JSX in the past, you may be used to replacing the &lt;code&gt;for&lt;/code&gt; attribute on the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;label&amp;gt;&lt;/code&gt;&lt;/a&gt; with &lt;code&gt;htmlFor&lt;/code&gt;, but Qwik’s compiler actually doesn’t require us to do that, so it’s fine as is.&lt;/p&gt;

&lt;p&gt;Next, we’ll want to replace the default form submission behavior. By default, when an HTML form is submitted, the browser will create an HTTP request by loading the URL provided in the form’s &lt;code&gt;action&lt;/code&gt; attribute. If none is provided, it will use the current URL. We want to avoid this page load and use JavaScript instead.&lt;/p&gt;

&lt;p&gt;If you’ve done this before, you may be familiar with the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault" rel="noopener noreferrer"&gt;&lt;code&gt;preventDefault&lt;/code&gt;&lt;/a&gt; method on the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Event" rel="noopener noreferrer"&gt;Event&lt;/a&gt; interface. As the name suggests, it prevents the default behavior for the event.&lt;/p&gt;

&lt;p&gt;There’s a challenge here due to &lt;a href="https://qwik.builder.io/docs/advanced/qwikloader/#events-and-the-qwikloader" rel="noopener noreferrer"&gt;how Qwik deals with event handlers&lt;/a&gt;. Unlike other frameworks, Qwik does not download all the JavaScript logic for the application upon first page load. Instead, it has a very thin client that intercepts user interactions and downloads the JavaScript event handlers on-demand.&lt;/p&gt;

&lt;p&gt;This asynchronous nature makes Qwik applications much faster to load, but introduces a challenge of dealing with event handlers asynchronously. It makes it impossible to prevent the default behavior the same way as synchronous event handlers that are downloaded and parsed before the user interactions.&lt;/p&gt;

&lt;p&gt;Fortunately, Qwik provides a way to prevent the default behavior by adding &lt;code&gt;preventdefault:{eventName}&lt;/code&gt; to the HTML tag. A very basic form example may look something like this:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt; &lt;span class="p"&gt;}&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;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;
      &lt;span class="na"&gt;preventdefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;
      &lt;span class="nx"&gt;onSubmit$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;contents&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;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;Did you notice that little &lt;a href="https://qwik.builder.io/docs/advanced/dollar/#the-dollar--sign" rel="noopener noreferrer"&gt;&lt;code&gt;$&lt;/code&gt;&lt;/a&gt; at the end of the &lt;code&gt;onSubmit$&lt;/code&gt; handler, there? Keep an eye out for those, because they are usually a hint to the developer that Qwik’s compiler is going to do something funny and transform the code. In this case, it’s due to that lazy-loading event handling system I mentioned above. If you plan on working with Qwik more, it’s worth &lt;a href="https://qwik.builder.io/docs/advanced/optimizer/" rel="noopener noreferrer"&gt;reading more about that here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incorporate the Fetch Request
&lt;/h2&gt;

&lt;p&gt;Now we have the tools in place to replace the default form submission with the fetch request we created above.&lt;/p&gt;

&lt;p&gt;What we want to do next is pull the data from the &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; into the body of the fetch request. We can do so with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData" rel="noopener noreferrer"&gt;&lt;code&gt;FormData&lt;/code&gt;&lt;/a&gt;, which expects a form element as an argument and provides an API to access a form control values through the control’s &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#name" rel="noopener noreferrer"&gt;&lt;code&gt;name&lt;/code&gt; attribute&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We can access the form element from the event’s &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Event/target" rel="noopener noreferrer"&gt;&lt;code&gt;target&lt;/code&gt;&lt;/a&gt; property, use it to create a new &lt;code&gt;FormData&lt;/code&gt; object, and use that to get the &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;/code&gt; value by referencing its &lt;code&gt;name&lt;/code&gt;, “prompt”. Plug that into the body of the fetch request we wrote above, and you might get something that looks like this:&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="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&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="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;
      &lt;span class="na"&gt;preventdefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;
      &lt;span class="nx"&gt;onSubmit$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prompt&lt;/span&gt;&lt;span class="dl"&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;body&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;model&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;gpt-3.5-turbo&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;messages&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&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;Authorization&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;Bearer OPENAI_API_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;body&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="nf"&gt;stringify&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="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;contents&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;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;In theory, you should now have a form on your page that, when submitted, sends the value from the textarea to the OpenAI API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Protect Your API Keys
&lt;/h2&gt;

&lt;p&gt;Although our HTTP request is working, there’s a glaring issue. Because it’s being constructed on the client side, anyone can open the browser dev tools and inspect the properties of the request. This includes the &lt;code&gt;Authorization&lt;/code&gt; header containing our API keys.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-91-1080x674.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-91-1080x674.png%3Fquality%3D100%26f%3Dauto%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I’ve blocked out my API token here with a red bar.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This would allow someone to steal our API tokens and make requests on our behalf, which could lead to abuse or higher charges on our account.&lt;/p&gt;

&lt;p&gt;Not good!!!&lt;/p&gt;

&lt;p&gt;The best way to prevent this is to move this API call to a backend server that we control that would work as a proxy. The frontend can make an unauthenticated request to the backend, and the backend would make the authenticated request to OpenAI and return the response to the frontend. But because users can’t inspect backend processes, they would not be able to see the Authentication header.&lt;/p&gt;

&lt;p&gt;So how do we move the fetch request to the backend?&lt;/p&gt;

&lt;p&gt;I’m so glad you asked!&lt;/p&gt;

&lt;p&gt;We’ve been mostly focusing on building the frontend with Qwik, the framework, but we also have access to use &lt;a href="https://qwik.builder.io/docs/qwikcity/" rel="noopener noreferrer"&gt;Qwik City&lt;/a&gt;, the full-stack meta-framework with tooling for file-based routing, route middleware, HTTP endpoints, and more.&lt;/p&gt;

&lt;p&gt;Of the various options Qwik City offers for running backend logic, my favorite is &lt;a href="https://qwik.builder.io/docs/action/" rel="noopener noreferrer"&gt;&lt;code&gt;routeAction$&lt;/code&gt;&lt;/a&gt;. It allows us to create a backend function that can be triggered from the client over HTTP (essentially an &lt;a href="https://en.wikipedia.org/wiki/Remote_procedure_call" rel="noopener noreferrer"&gt;RPC&lt;/a&gt; endpoint).&lt;/p&gt;

&lt;p&gt;The logic would follow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use&lt;/strong&gt; &lt;code&gt;routeAction$()&lt;/code&gt; to create an action.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Provide the backend logic as the parameter.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Programmatically execute the action’s&lt;/strong&gt; &lt;code&gt;submit()&lt;/code&gt; method.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified example could be:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt; &lt;span class="p"&gt;}&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;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routeAction$&lt;/span&gt; &lt;span class="p"&gt;}&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;@builder.io/qwik-city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routeAction&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;params&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;action on the server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;params&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;o&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&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;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAction&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;
      &lt;span class="na"&gt;preventdefault&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt;
      &lt;span class="nx"&gt;onSubmit$&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&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&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="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;contents&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/form&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&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="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I included a &lt;code&gt;JSON.stringify(action)&lt;/code&gt; at the end of the template because I think you should see what the returned &lt;a href="https://qwik.builder.io/api/qwik-city/#actionstore" rel="noopener noreferrer"&gt;&lt;code&gt;ActionStore&lt;/code&gt;&lt;/a&gt; looks like. It contains extra information like whether the action is running, what the submission values were, what the response status is, what the returned value is, and more.&lt;/p&gt;

&lt;p&gt;This is all very useful data that we get out of the box just by using an action, and it allows us to create more robust applications with less work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enhance the Experience
&lt;/h2&gt;

&lt;p&gt;Qwik City actions are cool, but they get even better when combined with Qwik’s &lt;a href="https://qwik.builder.io/docs/action/#using-actions-with-form" rel="noopener noreferrer"&gt;&lt;code&gt;&amp;lt;Form&amp;gt;&lt;/code&gt;&lt;/a&gt; component:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Under the hood, the component uses a native HTML element, so it will work without JavaScript.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When JS is enabled, the component will intercept the form submission and trigger the action in SPA mode, allowing to have a full SPA experience.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By replacing the HTML &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; element with Qwik’s &lt;code&gt;&amp;lt;Form&amp;gt;&lt;/code&gt; component, we no longer have to set up &lt;code&gt;preventdefault:submit&lt;/code&gt;, &lt;code&gt;onSubmit$&lt;/code&gt;, or call &lt;code&gt;action.submit()&lt;/code&gt;. We can just pass the action to the &lt;code&gt;Form&lt;/code&gt;‘s &lt;code&gt;action&lt;/code&gt; prop, and it’ll take care of the work for us. Additionally, it will work if JavaScript is not available for some reason (we could have done this with the HTML version as well, but it would have been more work).&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt; &lt;span class="p"&gt;}&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;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routeAction$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Form&lt;/span&gt; &lt;span class="p"&gt;}&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;@builder.io/qwik-city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routeAction&lt;/span&gt;&lt;span class="nf"&gt;$&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;action on the server&lt;/span&gt;&lt;span class="dl"&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;o&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;k&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&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;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAction&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;contents&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Form&lt;/span&gt;&lt;span class="err"&gt;&amp;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;So that’s an improvement for the developer experience. Let’s also improve the user experience.&lt;/p&gt;

&lt;p&gt;Within the &lt;code&gt;ActionStore&lt;/code&gt;, we have access to the &lt;code&gt;isRunning&lt;/code&gt; data which keeps track of whether the request is pending or not. It’s handy information we can use to let the user know when the request is in flight.&lt;/p&gt;

&lt;p&gt;We can do so by modifying the text of the submit button to say “Tell me” when it’s idle, then “One sec…” while it’s loading. I also like to assign the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-disabled" rel="noopener noreferrer"&gt;&lt;code&gt;aria-disabled&lt;/code&gt;&lt;/a&gt; attribute to match the &lt;code&gt;isRunning&lt;/code&gt; state. This will hint to assistive technology that it’s not ready to be clicked (though technically still can be). It can also be targeted with CSS to provide visual styles suggesting it’s not quite ready to be clicked again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;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;aria-disabled=&lt;/span&gt;&lt;span class="s"&gt;{state.isLoading}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  {state.isLoading ? 'One sec...' : 'Tell me'}
&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Show the Results
&lt;/h2&gt;

&lt;p&gt;Ok, we’ve done way too much work without actually seeing the results on the page. It’s time to change that. Let’s bring the &lt;code&gt;fetch&lt;/code&gt; request we prototyped earlier in the browser into our application.&lt;/p&gt;

&lt;p&gt;We can copy/paste the &lt;code&gt;fetch&lt;/code&gt; code right into the body of our action handler, but to access the user’s input data, we’ll need access to the form data that is submitted. Fortunately, any data passed to the &lt;code&gt;action.submit()&lt;/code&gt; method will be available to the action handler as the first parameter. It will be a serialized object where the keys correspond to the form control names.&lt;/p&gt;

&lt;p&gt;Note that I’ll be using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await" rel="noopener noreferrer"&gt;&lt;code&gt;await&lt;/code&gt;&lt;/a&gt; keyword in the body of the handler, which means I also have to tag the handler as an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function" rel="noopener noreferrer"&gt;&lt;code&gt;async&lt;/code&gt; function&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt; &lt;span class="p"&gt;}&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;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routeAction$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Form&lt;/span&gt; &lt;span class="p"&gt;}&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;@builder.io/qwik-city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routeAction&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="c1"&gt;// From &amp;lt;textarea name="prompt"&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&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;model&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;gpt-3.5-turbo&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;messages&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;role&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;user&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;content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&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;Authorization&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;Bearer OPENAI_API_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;body&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="nf"&gt;stringify&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="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="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="nf"&gt;json&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At the end of the action handler, we also want to return some data for the frontend. The OpenAI response comes back as JSON, but I think we might as well just return the text. If you remember from the response object we saw above, that data is located at &lt;code&gt;responseBody.choices[0].message.content&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If we set things up correctly, we should be able to access the action handler’s response in the &lt;code&gt;ActionStore&lt;/code&gt;‘s &lt;code&gt;value&lt;/code&gt; property. This means we can conditionally render it somewhere in the template like so:&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="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Use Environment Variables
&lt;/h2&gt;

&lt;p&gt;Alright, we’ve moved the OpenAI request to the backend, protected our API keys from prying eyes, we’re getting a (mediocre joke) response, and displaying it on the frontend. The app is working, but there’s still one more security issue to deal with.&lt;/p&gt;

&lt;p&gt;It’s generally a bad idea to hard code API keys into your source code, for a number of reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It means you can’t share the repo publicly without exposing your keys.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You may run up API usage during development, testing, and staging.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Changing API keys requires code changes and re-deploys.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;You’ll need to regenerate API keys anytime someone leaves the org.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A better system is to use &lt;a href="https://en.wikipedia.org/wiki/Environment_variable" rel="noopener noreferrer"&gt;&lt;strong&gt;environment variables&lt;/strong&gt;&lt;/a&gt;. With environment variables, you can provide the API keys only to the systems and users that need access to them.&lt;/p&gt;

&lt;p&gt;For example, you can make an environment variable called &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; with the value of your OpenAI key for only the production environment. This way, only developers with direct access to that environment would be able to access it. This greatly reduces the likelihood of the API keys leaking, it makes it easier to share your code openly, and because you are limiting access to the keys to the least number of people, you don’t need to replace keys as often because someone left the company.&lt;/p&gt;

&lt;p&gt;In Node.js, it’s common to set environment variables from the command line (&lt;code&gt;ENV_VAR=example npm start&lt;/code&gt;) or with the popular &lt;a href="https://www.npmjs.com/package/dotenv" rel="noopener noreferrer"&gt;&lt;code&gt;dotenv&lt;/code&gt;&lt;/a&gt; package. Then, in your server-side code, you can access environment variables using &lt;code&gt;process.env.ENV_VAR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Things work slightly differently with Qwik.&lt;/p&gt;

&lt;p&gt;Qwik can target different JavaScript runtimes (not just Node), and accessing environment variables via &lt;a href="https://nodejs.org/api/process.html#processenv" rel="noopener noreferrer"&gt;&lt;code&gt;process.env&lt;/code&gt;&lt;/a&gt; is a Node-specific concept. To make things more runtime-agnostic, Qwik provides access to environment variables through a &lt;a href="https://qwik.builder.io/api/qwik-city-middleware-request-handler/#requestevent" rel="noopener noreferrer"&gt;&lt;code&gt;RequestEvent&lt;/code&gt;&lt;/a&gt; object which is available as the second parameter to the route action handler function.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routeAction$&lt;/span&gt; &lt;span class="p"&gt;}&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;@builder.io/qwik-city&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routeAction&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;param&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&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;envVariableValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ENV_VARIABLE_NAME&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="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;envVariableValue&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So that’s how we access environment variables, but how do we set them?&lt;/p&gt;

&lt;p&gt;Unfortunately, for production environments, setting environment variables will differ depending on the platform. For a standard server &lt;a href="https://www.linode.com/products/dedicated-cpu/" rel="noopener noreferrer"&gt;VPS&lt;/a&gt;, you can still set them with the terminal as you would in Node (&lt;code&gt;ENV_VAR=example npm start&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;In development, we can alternatively create a &lt;code&gt;local.env&lt;/code&gt; file containing our environment variables, and they will be automatically assigned for us. This is convenient since we spend a lot more time starting the development environment, and it means we can provide the appropriate API keys only to the people who need them.&lt;/p&gt;

&lt;p&gt;So after you create a &lt;code&gt;local.env&lt;/code&gt; file, you can assign the &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; variable to your API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPENAI_API_KEY="your-api-key"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(You may need to restart your dev server)&lt;/p&gt;

&lt;p&gt;Then we can access the environment variable through the &lt;a href="https://qwik.builder.io/api/qwik-city-middleware-request-handler/#requestevent" rel="noopener noreferrer"&gt;&lt;code&gt;RequestEvent&lt;/code&gt;&lt;/a&gt; parameter. With that, we can replace the hard-coded value in our &lt;code&gt;fetch&lt;/code&gt; request’s Authorization header with the variable using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals" rel="noopener noreferrer"&gt;Template Literals&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;const&lt;/span&gt; &lt;span class="nx"&gt;usePromptAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routeAction&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&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;OPENAI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="dl"&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&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="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&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="nf"&gt;stringify&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="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="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="nf"&gt;json&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more details on environment variables in Qwik, &lt;a href="https://qwik.builder.io/docs/env-variables/#environment-variables" rel="noopener noreferrer"&gt;see their documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;When a user submits the form, the default behavior is intercepted by Qwik’s optimizer which lazy loads the event handler.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The event handler uses JavaScript to create an HTTP request containing the form data to send to the server to be handled by the route’s action.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The route’s action handler will have access to the form data in the first parameter and can access environment variables from the second parameter (a&lt;/strong&gt; &lt;code&gt;RequestEvent&lt;/code&gt; object).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Inside the route’s action handler, we can construct and send the HTTP request to OpenAI using the data we got from the form, and the API keys we pulled from the environment variables.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;With the OpenAI response, we can prepare the data to send back to the client.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The client receives the response from the action and can update the page accordingly.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here’s what my final component looks like, including some Tailwind classes and a slightly different template.&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;component$&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;routeAction$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Form&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@builder.io/qwik-city&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;usePromptAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;routeAction&lt;/span&gt;&lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&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;OPENAI_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;requestEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="dl"&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;prompt&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gpt-3.5-turbo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.openai.com/v1/chat/completions&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;post&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&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;Content-Type&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;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_KEY&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="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&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="nf"&gt;stringify&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="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="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="nf"&gt;json&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="nf"&gt;$&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;action&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;usePromptAction&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;main&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;max-w-4xl mx-auto p-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;h1&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text-4xl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Hi&lt;/span&gt; &lt;span class="err"&gt;👋&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/h1&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Form&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;grid gap-4&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Prompt&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/label&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;textarea&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nx"&gt;Tell&lt;/span&gt; &lt;span class="nx"&gt;me&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;joke&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/textarea&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;submit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isRunning&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="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;isRunning&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;One sec...&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;Tell me&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/button&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Form&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mt-4 border border-2 rounded-lg p-4 bg-[canvas]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/p&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/article&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/main&lt;/span&gt;&lt;span class="err"&gt;&amp;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;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;All right! We’ve gone from a script that uses AI to get mediocre jokes to a full-blown application that securely makes HTTP requests to a backend that uses AI to get mediocre jokes and sends them back to the frontend to put those mediocre jokes on a page.&lt;/p&gt;

&lt;p&gt;You should feel pretty good about yourself.&lt;/p&gt;

&lt;p&gt;But not too good, because there’s still room to improve.&lt;/p&gt;

&lt;p&gt;In our application, we are sending a request and getting an AI response, but we are waiting for the entirety of the body of that response to be generated before showing it to the users. And these AI responses can take a while to complete.&lt;/p&gt;

&lt;p&gt;If you’ve used AI chat tools in the past, you may be familiar with the experience where it looks like it’s typing the responses to you, one word at a time, as they’re being generated. This doesn’t speed up the total request time, but it does get some information back to the user much sooner and feels like a faster experience.&lt;/p&gt;

&lt;p&gt;In the next post, we’ll learn how to build that same feature using HTTP streams, which are fascinating and powerful but also can be kind of confusing. So I’m going to dedicate an entire post just to that.&lt;/p&gt;

&lt;p&gt;I hope you’re enjoying this series and plan to stick around. In the meantime, have fun generating some mediocre jokes.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/" rel="noopener noreferrer"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/" rel="noopener noreferrer"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/" rel="noopener noreferrer"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/" rel="noopener noreferrer"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/" rel="noopener noreferrer"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil" rel="noopener noreferrer"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil" rel="noopener noreferrer"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>programming</category>
      <category>ai</category>
    </item>
    <item>
      <title>AI for Web Devs: Project Introduction &amp; Setup</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Fri, 12 Jan 2024 15:39:00 +0000</pubDate>
      <link>https://dev.to/austingil/ai-for-web-devs-project-introduction-setup-4jhk</link>
      <guid>https://dev.to/austingil/ai-for-web-devs-project-introduction-setup-4jhk</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/gUgRD0sRoCU"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;If you’re anything like me, you’ve noticed the massive boom in &lt;a href="https://austingil.com/category/ai/" rel="noopener noreferrer"&gt;AI&lt;/a&gt; technology. It promises to disrupt not just software engineering but every industry.&lt;/p&gt;

&lt;p&gt;THEY’RE COMING FOR US!!!&lt;/p&gt;

&lt;p&gt;Just kidding ;P&lt;/p&gt;

&lt;p&gt;I’ve been bettering my understanding of what these tools are and how they work, and decided to create a tutorial series for web developers to learn how to incorporate AI technology into web apps.&lt;/p&gt;

&lt;p&gt;In this series, we’ll learn how to integrate &lt;a href="https://openai.com/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;‘s AI services into an application built with &lt;a href="https://qwik.builder.io/" rel="noopener noreferrer"&gt;Qwik&lt;/a&gt;, a &lt;a href="https://austingil.com/category/javascript/" rel="noopener noreferrer"&gt;JavaScript&lt;/a&gt; framework focused on the concept of &lt;a href="https://qwik.builder.io/docs/concepts/resumable/" rel="noopener noreferrer"&gt;resumability&lt;/a&gt; (this will be relevant to understand later).&lt;/p&gt;

&lt;p&gt;Here’s what the series outline looks like:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/" rel="noopener noreferrer"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/" rel="noopener noreferrer"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/" rel="noopener noreferrer"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/" rel="noopener noreferrer"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/" rel="noopener noreferrer"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We’ll get into the specifics of OpenAI and Qwik where it makes sense, but I will mostly focus on general-purpose knowledge, tooling, and implementations that should apply to whatever framework or toolchain you are using. We’ll be working as closely to fundamentals as we can, and I’ll point out which parts are unique to this app.&lt;/p&gt;

&lt;p&gt;Here’s a little sneak preview.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-93-1080x751.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-93-1080x751.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Screenshot of versus.austingil.com"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I thought it would be cool to build an app that takes two opponents and uses AI to determine who would win in a hypothetical fight. It provides some explanation and the option to create an AI-generated image. Sometimes the results come out a little wonky, but that’s what makes it fun.&lt;/p&gt;

&lt;p&gt;I hope you’re excited to get started because in this first post we are mostly going to work on…&lt;/p&gt;

&lt;p&gt;Boilerplate 😒&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before we start building anything, we have to cover a couple of prerequisites. Qwik is a JavaScript framework, so we will have to have &lt;a href="https://nodejs.org/en" rel="noopener noreferrer"&gt;Node.js&lt;/a&gt; (and NPM) installed. You can download the most recent version, but anything above version v16.8 should work. I’ll be using version 20.&lt;/p&gt;

&lt;p&gt;Next, we’ll also need an &lt;a href="https://platform.openai.com/signup?launch" rel="noopener noreferrer"&gt;OpenAI account&lt;/a&gt; to have access to their &lt;a href="https://platform.openai.com/docs/introduction" rel="noopener noreferrer"&gt;API&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At the end of the series, we will deploy our applications to a VPS (Virtual Private Server). The steps we follow should be the same regardless of what provider you choose. I’ll be using Akamai’s cloud computing services (formerly Linode). New users can go to &lt;a href="https://austingil.com/category/ai/" rel="noopener noreferrer"&gt;linode.com/austingil&lt;/a&gt; and get $100 in free credits to get started with Akamai.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Qwik App
&lt;/h2&gt;

&lt;p&gt;Assuming we have the prerequisites out of the way, we can open a command line terminal and run the command: &lt;code&gt;npm create qwik@latest&lt;/code&gt;. This will run the Qwik CLI that will help us bootstrap our application.&lt;/p&gt;

&lt;p&gt;It will ask you a series of configuration questions, then generate the project for you. Here’s what my answers looked like:&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-86-1080x619.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-86-1080x619.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Let's create a  Qwik App  ✨ (v1.2.7)Where would you like to create your new project? (Use '.' or './' for current directory): versusCreating new project in /home/austin/code/versusSelect a starter: Empty AppWould you like to install npm dependencies? YesInitialize a new git repository? YesFinishing the install. Wanna hear a joke? YesWhat did the big flower say to the littler flower? Hi, bud!"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If everything worked, open up the project and start exploring.&lt;/p&gt;

&lt;p&gt;Inside the project folder, you’ll notice some important files and folders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/src&lt;/code&gt;: contains all application business logic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/src/components&lt;/code&gt;: contains reusable components to build our app with.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/src/routes&lt;/code&gt;: responsible for Qwik’s file-based routing. Each folder represents a route (can be a page or API endpoint). To make a page, drop a &lt;code&gt;index.{jsx|tsx}&lt;/code&gt; file in the route’s folder.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/src/root.tsx&lt;/code&gt;: this file exports the root component responsible for generating the &lt;a href="https://austingil.com/category/html/" rel="noopener noreferrer"&gt;HTML&lt;/a&gt; document root.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Start Development
&lt;/h2&gt;

&lt;p&gt;Qwik uses &lt;a href="https://vitejs.dev/" rel="noopener noreferrer"&gt;Vite&lt;/a&gt; as a bundler, which is convenient because Vite has a built-in development server. It supports running our application locally, and updating the browser when files changes.&lt;/p&gt;

&lt;p&gt;To start the development server, we can open our project in a terminal and execute the command &lt;code&gt;npm run dev&lt;/code&gt;. With the dev server running, you can open the browser and head to &lt;a href="https://austingil.com/category/ai/" rel="noopener noreferrer"&gt;&lt;code&gt;http://localhost:5173&lt;/code&gt;&lt;/a&gt; and you should see a very basic app.&lt;/p&gt;

&lt;p&gt;Any time we make changes to our app, we should see those changes reflected almost immediately in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add Styling
&lt;/h2&gt;

&lt;p&gt;This project won’t focus too much on styling, so this section is totally optional if you want to do your own thing. To keep things simple, I’ll use &lt;a href="https://tailwindcss.com/" rel="noopener noreferrer"&gt;Tailwind&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The Qwik CLI makes it easy to add the necessary changes, by executing the terminal command, &lt;code&gt;npm run qwik add&lt;/code&gt;. This will prompt you with several available Qwik plugins to choose from.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-87-1080x894.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-87-1080x894.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="What integration would you like to add?- Adapter: Cloudflare Pages- Adapter: AWS Lambda- Adapter: Azure Static Web Apps- Adapter: Netlify Edge- Adapter: Vercel Edge- Adapter: Google Cloud Run server- Adapter: Deno Server- Adapter: Node.js Express Server- Adapter: Node.js Fastify Server- Adapter: Node.js Server- Adapter: Static site (.html files)- Integration: Builder.io- Integration: Cypress- Integration: Storybook- Integration: Auth.js (authentication)- Integration: Orama (full-text search engine)- Integration: PandaCSS (styling)- Integration: Playwright (E2E Test)- Integration: PostCSS (styling)- Integration: Prisma (Database ORM)- Integration: Styled-Vanilla-Extract (styling)- Integration: Tailwind (styling) (Use Tailwind in your Qwik app)- Integration: Turso (database)- Integration: Vitest (Unit Test)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can use your arrow keys to move down to the Tailwind plugin and press Enter. Then it will show you the changes it will make to your codebase and ask for confirmation. As long as it looks good, you can hit Enter, once again.&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-88.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-88.png%3Fquality%3D100%26f%3Dauto%2520align%3D" alt="Ready? Add tailwind to your app?Modify- package.json- .vscode/settings.json- src/global.cssCreate- postcss.config.js- tailwind.config.jsInstall npm dependencies:- autoprefixer ^10.4.14- postcss 8.4.27- tailwindcss 3.3.3Ready to apply the tailwind updates to your app?- Yes looks good, finish update!"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For my projects, I also like to have a consistent theme, so I keep a file in my &lt;a href="https://github.com/austingil/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; to copy and paste styles from. Obviously, if you want your own theme, you can ignore this step, but if you want your project to look as amazing as mine, copy the styles from &lt;a href="https://github.com/AustinGil/utils/blob/master/css/theme.css" rel="noopener noreferrer"&gt;this file on GitHub&lt;/a&gt; into the &lt;code&gt;/src/global.css&lt;/code&gt; file. You can replace the old styles, but leave the Tailwind directives in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prepare Homepage
&lt;/h2&gt;

&lt;p&gt;The last thing we’ll do today to get the project in a good starting point is make some changes to the homepage. This means making changes to &lt;code&gt;/src/routes/index.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By default, this file starts out with some very basic text, and an example for modifying the HTML &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; by exporting a &lt;code&gt;head&lt;/code&gt; variable. The changes I want to make include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removing the&lt;/strong&gt; &lt;code&gt;head&lt;/code&gt; export.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Removing all text except the&lt;/strong&gt; &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt;. Feel free to add your own page title text.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adding some Tailwind classes to center the content and make the&lt;/strong&gt; &lt;code&gt;&amp;lt;h1&amp;gt;&lt;/code&gt; larger.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Wrapping the content with a&lt;/strong&gt; &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; tag to make it more semantic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adding Tailwind classes to the&lt;/strong&gt; &lt;code&gt;&amp;lt;main&amp;gt;&lt;/code&gt; tag to add some padding and center the contents.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are all minor changes that aren’t strictly necessary, but I think it will provide a nice starting point for building out our app in the next post.&lt;/p&gt;

&lt;p&gt;Here’s what the file looks like after my changes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { component$ } from "@builder.io/qwik";

export default component$(() =&amp;gt; {
  return (
    &amp;lt;main class="max-w-4xl mx-auto p-4"&amp;gt;
      &amp;lt;h1 class="text-6xl"&amp;gt;Hi 👋&amp;lt;/h1&amp;gt;
    &amp;lt;/main&amp;gt;
  );
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And in the browser, it looks like this:&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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-89-1080x581.png%3Fquality%3D100%26f%3Dauto%2520align%3D" 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%2Fcdn.statically.io%2Fimg%2Faustingil.com%2Fwp-content%2Fuploads%2Fimage-89-1080x581.png%3Fquality%3D100%26f%3Dauto%2520align%3D"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;That’s all we’ll cover today. Again, this post was mostly focused on getting the boilerplate stuff out of the way so that the next post can be dedicated to integrating OpenAI’s API into our project.&lt;/p&gt;

&lt;p&gt;With that in mind, I encourage you to take a moment to think about some AI app ideas that you might want to build. There will be a lot of flexibility for you to put your own spin on things.&lt;/p&gt;

&lt;p&gt;I’m excited to see what you come up with, and if you would like to explore the code in more detail, I’ll post it on my GitHub account at &lt;a href="https://austingil.com/category/ai/" rel="noopener noreferrer"&gt;github.com/austingil/versus&lt;/a&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-set-up/" rel="noopener noreferrer"&gt;&lt;strong&gt;Intro &amp;amp; Setup&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-first-http-request/" rel="noopener noreferrer"&gt;&lt;strong&gt;Your First AI Prompt&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-streaming/" rel="noopener noreferrer"&gt;&lt;strong&gt;Streaming Responses&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-neural-networks-llms-gpts/" rel="noopener noreferrer"&gt;&lt;strong&gt;How Does AI Work&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-prompt-engineering/" rel="noopener noreferrer"&gt;&lt;strong&gt;Prompt Engineering&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-image-generation/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI-Generated Images&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-bugs-security-reliability/" rel="noopener noreferrer"&gt;&lt;strong&gt;Security &amp;amp; Reliability&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/ai-for-web-devs-deploying/" rel="noopener noreferrer"&gt;&lt;strong&gt;Deploying&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil" rel="noopener noreferrer"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/" rel="noopener noreferrer"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil" rel="noopener noreferrer"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="https://austingil.com/category/ai/" rel="noopener noreferrer"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>ai</category>
      <category>programming</category>
    </item>
    <item>
      <title>File Upload Security and Malware Protection</title>
      <dc:creator>Austin Gil</dc:creator>
      <pubDate>Fri, 26 May 2023 15:43:58 +0000</pubDate>
      <link>https://dev.to/austingil/file-upload-security-and-malware-protection-23fn</link>
      <guid>https://dev.to/austingil/file-upload-security-and-malware-protection-23fn</guid>
      <description>&lt;p&gt;Today we’re going to be wrapping up this series on file uploads for the web. If you’ve been following along, you should now be familiar with &lt;a href="https://austingil.com/uploading-files-with-html/"&gt;enabling file uploads on the front end&lt;/a&gt; and &lt;a href="https://austingil.com/file-uploads-in-node/"&gt;the back end&lt;/a&gt;. We’ve covered architectural decisions to &lt;a href="https://austingil.com/upload-to-s3/"&gt;reduce cost on where we host our files&lt;/a&gt; and &lt;a href="https://austingil.com/file-uploads-cdn/"&gt;improve the delivery performance&lt;/a&gt;. So I thought we would wrap up the series today by covering security as it relates to file uploads.&lt;/p&gt;

&lt;p&gt;In case you’d like to go back and revisit any earlier blogs in the series, here’s a list of what we’ve covered so far:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/uploading-files-with-html/"&gt;&lt;strong&gt;Upload files with HTML&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/upload-files-with-javascript/"&gt;&lt;strong&gt;Upload files with JavaScript&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/file-uploads-in-node/"&gt;&lt;strong&gt;Receive uploads in Node.js (Nuxt.js)&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/upload-to-s3/"&gt;&lt;strong&gt;Optimize storage costs with Object Storage&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/file-uploads-cdn/"&gt;&lt;strong&gt;Optimize performance with a CDN&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/file-upload-security-and-malware-protection/"&gt;&lt;strong&gt;Upload security &amp;amp; malware protection&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/FARa5NHZh90"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Anytime I discuss the topic of security, I like to consult the experts at &lt;a href="http://OWASP.org"&gt;OWASP.org&lt;/a&gt;. Conveniently, they have a &lt;a href="https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html"&gt;File Upload Cheat Sheet&lt;/a&gt;, which outlines several attack vectors related to file uploads and steps to mitigate them.&lt;/p&gt;

&lt;p&gt;Today we’ll walk through this cheat sheet and how to implement some of their recommendations into an existing application.&lt;/p&gt;

&lt;p&gt;For a bit of background, the application has a frontend with a form that has a single file input that uploads that file to a backend.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZBCBgbmT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-79.png%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZBCBgbmT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-79.png%2520align%3D%2522left%2522" alt="" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The backend is powered by &lt;a href="https://nuxt.com/"&gt;Nuxt.js&lt;/a&gt;‘ &lt;a href="https://nuxt.com/docs/guide/directory-structure/server"&gt;Event Handler API&lt;/a&gt;, which receives an incoming request as an “&lt;code&gt;event&lt;/code&gt;” object, detects whether it’s a &lt;code&gt;multipart/form-data&lt;/code&gt; request (always true for file uploads), and passes the underlying &lt;a href="https://nodejs.org/"&gt;Node.js&lt;/a&gt; request object (or &lt;a href="https://nodejs.org/api/http.html"&gt;&lt;code&gt;IncomingMessage&lt;/code&gt;&lt;/a&gt;) to this custom function called &lt;code&gt;parseMultipartNodeRequest&lt;/code&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;import&lt;/span&gt; &lt;span class="nx"&gt;formidable&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;formidable&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cm"&gt;/* global defineEventHandler, getRequestHeaders, readBody */&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * @see https://nuxt.com/docs/guide/concepts/server-engine
 * @see https://github.com/unjs/h3
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineEventHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;let&lt;/span&gt; &lt;span class="nx"&gt;body&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;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;content-type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multipart/form-data&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;parseMultipartNodeRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;readBody&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&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="nf"&gt;log&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&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;All the code we’ll be focusing on today will live within this &lt;code&gt;parseMultipartNodeRequest&lt;/code&gt; function. And since it works with the Node.js primitives, everything we do should work in any Node environment, regardless of whether you’re using Nuxt or Next or any other sort of framework or library.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;parseMultipartNodeRequest&lt;/code&gt; we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Create a new Promise&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Instantiate a&lt;/strong&gt; &lt;code&gt;multipart/form-data&lt;/code&gt; parser using a library called &lt;a href="https://github.com/node-formidable/formidable/"&gt;formidable&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Parse the incoming Node request object&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The parser writes files to their storage location&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The parser provides information about the fields and the files in the request&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once it’s done parsing, we resolve &lt;code&gt;parseMultipartNodeRequest&lt;/code&gt;‘s Promise with the fields and files.&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="cm"&gt;/**
 * @param {import('http').IncomingMessage} req
 */&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseMultipartNodeRequest&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formidable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;multiples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&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="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;files&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="k"&gt;if &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="nf"&gt;reject&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="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That’s what we’re starting with today, but if you want a better understanding of the low-level concepts for handling &lt;code&gt;multipart/form-data&lt;/code&gt; requests in Node, check out, “&lt;a href="https://austingil.com/file-uploads-in-node/"&gt;Handling File Uploads on the Backend in Node.js (&amp;amp; Nuxt)&lt;/a&gt;.” It covers low level topics like chunks, streams, and buffers, then shows how to use a library instead of writing one from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Securing Uploads
&lt;/h2&gt;

&lt;p&gt;With our app set up and running, we can start to implement some of the recommendations from OWASP’s cheat sheet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extension Validation
&lt;/h3&gt;

&lt;p&gt;With this technique, we check the uploading file name extensions and only allow files with the allowed extension types into our system.&lt;/p&gt;

&lt;p&gt;Fortunately, this is pretty easy to implement with formidable. When we initialize the library, we can pass a &lt;code&gt;filter&lt;/code&gt; configuration option which should be a function that has access to a &lt;code&gt;file&lt;/code&gt; object parameter that provides some details about the file, including the original file name. The function must return a boolean that tells formidable whether to allow writing it to the storage location or not.&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formidable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// other config options&lt;/span&gt;
  &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// filter logic here&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 could check &lt;code&gt;file.originalFileName&lt;/code&gt; against a regular expression that tests whether a string ends with one of the allowed file extensions. For any upload that doesn’t pass the test, we can return &lt;code&gt;false&lt;/code&gt; to tell formidable to skip that file and for everything else, we can return &lt;code&gt;true&lt;/code&gt; to tell formidable to write the file to the system.&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formidable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// other config options&lt;/span&gt;
  &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&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;originalFilename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalFilename&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Enforce file ends with allowed extension&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowedExtensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;jpe&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;g|png|gif|avif|webp|svg|txt&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/i&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;allowedExtensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalFilename&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="kc"&gt;false&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="kc"&gt;true&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;
  
  
  Filename Sanitization
&lt;/h3&gt;

&lt;p&gt;Filename sanitization is a good way to protect against file names that may be too long or include characters that are not acceptable for the operating system.&lt;/p&gt;

&lt;p&gt;The recommendation is to generate a new filename for any upload. Some options may be a random string generator, a UUID, or some sort of hash.&lt;/p&gt;

&lt;p&gt;Once again, formidable makes this easy for us by providing a &lt;code&gt;filename&lt;/code&gt; configuration option. And once again it should be a function that provides details about the file, but this time it expects a string.&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formidable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// other config options&lt;/span&gt;
  &lt;span class="nf"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// return some random string&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 can actually skip this step because formidable’s default behavior is to generate a random hash for every upload. So we’re already following best practices just by using the default settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upload and Download Limits
&lt;/h3&gt;

&lt;p&gt;Next, we’ll tackle upload limits. This protects our application from running out of storage, limits how much we pay for storage, and limits how much data could be transferred if those files get downloaded, which may also affect how much we have to pay.&lt;/p&gt;

&lt;p&gt;Once again, we get some basic protection just by using formidable because it sets a default value of 200 megabytes as the maximum file upload size.&lt;/p&gt;

&lt;p&gt;If we want, we could override that value with a custom &lt;code&gt;maxFileSize&lt;/code&gt; configuration option. For example, we could set it to 10 megabytes like this:&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formidable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// other config options&lt;/span&gt;
  &lt;span class="na"&gt;maxFileSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The right value to choose is highly subjective based on your application needs. For example, an application that accepts high-definition video files will need a much higher limit than one that expects only PDFs.&lt;/p&gt;

&lt;p&gt;You’ll want to choose the lowest conservative value without being so low that it hinders normal users.&lt;/p&gt;

&lt;h3&gt;
  
  
  File Storage Location
&lt;/h3&gt;

&lt;p&gt;It’s important to be intentional about where uploaded files get stored. The top recommendation is to store uploaded files in a completely different location than where your application server is running.&lt;/p&gt;

&lt;p&gt;That way, if malware does get into the system, it will still be quarantined without access to the running application. This can prevent access to sensitive user information, environment variables, and more.&lt;/p&gt;

&lt;p&gt;In one of my previous posts, “&lt;a href="https://austingil.com/upload-to-s3/"&gt;Stream File Uploads to S3 Object Storage and Reduce Costs&lt;/a&gt;,” I showed how to stream file uploads to an object storage provider. So it’s not only more cost-effective, but it’s also more secure.&lt;/p&gt;

&lt;p&gt;But if storing files on a different host isn’t an option, the next best thing we can do is make sure that uploaded files do not end up in the root folder on the application server.&lt;/p&gt;

&lt;p&gt;Again, formidable handles this by default. It stores any uploaded files in the operating system’s temp folder. That’s good for security, but if you want to access those files later on, the temp folder is probably not the best place to store them.&lt;/p&gt;

&lt;p&gt;Fortunately, there’s another formidable configuration setting called &lt;code&gt;uploadDir&lt;/code&gt; to explicitly set the upload location. It can be either a relative path or an absolute path.&lt;/p&gt;

&lt;p&gt;So, for example, I may want to store files in a folder called “/uploads” inside my project folder. This folder must already exist, and if I want to use a relative path, it must be relative to the application runtime (usually the project root). That being the case, I can set the config option like this:&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formidable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// other config options&lt;/span&gt;
  &lt;span class="na"&gt;uploadDir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./uploads&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;h3&gt;
  
  
  Content-Type Validation
&lt;/h3&gt;

&lt;p&gt;Content-Type validation is important to ensure that the uploaded files match a given list of allowed &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types"&gt;MIME-types&lt;/a&gt;. It’s similar to extension validation, but it’s important to also check a file’s MIME-type because it’s easy for an attacker to simply rename a file to include a file extension that’s in our allowed list.&lt;/p&gt;

&lt;p&gt;Looking back at formidable’s filter function, we’ll see that it also provides us with the file’s MIME-type. So we could add some logic enforces the file MIME-type matches our allow list.&lt;/p&gt;

&lt;p&gt;We could modify our old function to also filter out any upload that is not an image.&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;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;formidable&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="c1"&gt;// other config options&lt;/span&gt;
  &lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&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;originalFilename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;originalFilename&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Enforce file ends with allowed extension&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allowedExtensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.(&lt;/span&gt;&lt;span class="sr"&gt;jpe&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;g|png|gif|avif|webp|svg|txt&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;$/i&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;allowedExtensions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;originalFilename&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="kc"&gt;false&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;mimetype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mimetype&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// Enforce file uses allowed mimetype&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mimetype&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mimetype&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, this would be great in theory, but the reality is that &lt;a href="https://github.com/node-formidable/formidable/issues/749"&gt;formidable actually generates the file’s MIME-type information based on the file extension&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That makes it no more useful than our extension validation. It’s unfortunate, but it also makes sense and is likely to remain the case.&lt;/p&gt;

&lt;p&gt;formidable’s filter function is designed to prevent files from being written to disk. It runs as it’s parsing uploads. But the only reliable way to know a file’s MIME-type is by checking the file’s contents. And you can only do that after the file has already been written to the disk.&lt;/p&gt;

&lt;p&gt;So we technically haven’t solved this issue yet, but checking file contents actually brings us to the next issue, file content validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intermission
&lt;/h3&gt;

&lt;p&gt;Before we get into that, let’s check the current functionality. I can upload several files, including three JPEGs and one text file (note that one of the JPEGs is quite large).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--x8J_SQlE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-80.png%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--x8J_SQlE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-80.png%2520align%3D%2522left%2522" alt="" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When I upload this list of files, I’ll get a failed request with a status code of 500. The server console reports the error is because the maximum allowed file size was exceeded.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ZbwaNd9N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-81-1080x374.png%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ZbwaNd9N--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-81-1080x374.png%2520align%3D%2522left%2522" alt='Server console reporting the error, "[nuxt] [request error] [unhandled] [500] options.maxFileSize (10485760 bytes) exceeded, received 10490143 bytes of file data"' width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is good.&lt;/p&gt;

&lt;p&gt;We’ve prevented a file from being uploaded into our system that exceeds the maximum file limit size (we should probably do a better job of handling errors on the backend, but that’s a job for another day).&lt;/p&gt;

&lt;p&gt;Now, what happens when we upload all those files except the big one?&lt;/p&gt;

&lt;p&gt;No error.&lt;/p&gt;

&lt;p&gt;And looking in the “uploads” folder, we’ll see that despite uploading three files, only two were saved. The &lt;code&gt;.txt&lt;/code&gt; file did not get past our file extension filter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--peArHanY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-82.png%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--peArHanY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-82.png%2520align%3D%2522left%2522" alt="" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We’ll also notice that the names of the two saved files are random hash values. Once again, that’s thanks to formidable default behavior.&lt;/p&gt;

&lt;p&gt;Now there’s just one problem. One of those two successful uploads came from the “bad-dog.jpeg” file I selected. That file was actually a copy of the “bad-dog.txt” that I renamed. And THAT file actually contains malware 😱😱😱&lt;/p&gt;

&lt;p&gt;We can prove it by running one of the most popular Linux antivirus tools on the uploads folder, &lt;a href="https://docs.clamav.net/manual/Usage/Scanning.html"&gt;ClamScan&lt;/a&gt;. Yes, ClamScan is a real thing. Yes, that’s its real name. No, I don’t know why they called it that. Yes, I know what it sounds like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--28Fuauq5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-83-1080x425.png%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--28Fuauq5--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-83-1080x425.png%2520align%3D%2522left%2522" alt="" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;(Side note: The file I used was created for testing malware software. So it’s harmless, but it’s designed to trigger malware scanners. But that meant I had to get around browser warnings, virus scanner warnings, firewall blockers, AND angry emails from our IT department just to get a copy. So you better learn something.)&lt;/p&gt;

&lt;p&gt;OK, &lt;strong&gt;now&lt;/strong&gt; let’s talk about file content validation.&lt;/p&gt;

&lt;h3&gt;
  
  
  File Content Validation
&lt;/h3&gt;

&lt;p&gt;File content validation is a fancy way of saying, “scan the file for malware”, and it’s one of the more important security steps you can take when accepting file uploads.&lt;/p&gt;

&lt;p&gt;We used ClamScan above, so now you might be thinking, “Aha, why don’t I just scan the files as formidable parses them?”&lt;/p&gt;

&lt;p&gt;Similar to MIME-type checking, malware scanning can only happen after the file has already been written to disc. Additionally, scanning file contents can take a long time. Far longer than is appropriate in a request-response cycle. You wouldn’t want to keep the user waiting that long.&lt;/p&gt;

&lt;p&gt;So we have two potential problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;By the time we can start scanning a file for malware, it’s already on our server.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;We can’t wait for scans to finish before responding to user’s upload requests.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bummer…&lt;/p&gt;

&lt;h2&gt;
  
  
  Malware Scanning Architecture
&lt;/h2&gt;

&lt;p&gt;Running a malware scan on every single upload request is probably not an option, but there are solutions. Remember that the goal is to protect our application from malicious uploads as well as to protect our users from malicious downloads.&lt;/p&gt;

&lt;p&gt;Instead of scanning uploads during the request-response cycle, we could accept all uploaded files, store them in a safe location, and add a record in a database containing the file’s metadata, storage location, and a flag to track whether the file has been scanned.&lt;/p&gt;

&lt;p&gt;Next, we could schedule a background process that locates and scans all the records in the database for unscanned files. If it finds any malware, it could remove it, quarantine it, and/or notify us. For all the clean files, it can update their respective database records to mark them as scanned.&lt;/p&gt;

&lt;p&gt;Lastly, there are considerations to make for the front end. We’ll likely want to show any previously uploaded files, but we have to be careful about providing access to potentially dangerous ones. Here are a couple different options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;After an upload, only show the file information to the user that uploaded it, letting them know that it won’t be available to others until after it’s been scanned. You may even email them when it’s complete.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;After an upload, show the file to every user, but do not provide a way to download the file until after it has been scanned. Include some messaging to tell users the file is pending a scan, but they can still see the file’s metadata.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Which option is right for you really depends on your application use case. And of course, these examples assume your application already has a database and the ability to schedule background tasks.&lt;/p&gt;

&lt;p&gt;It’s also worth mentioning here that one of the OWASP recommendations was to limit file upload capabilities to authenticated users. This makes it easier to track and prevent abuse.&lt;/p&gt;

&lt;p&gt;Unfortunately, databases, user accounts, and background tasks all require more time than I have to cover in today’s article, but I hope these concepts give you more ideas on how you can improve your upload security methods.&lt;/p&gt;

&lt;h2&gt;
  
  
  Block Malware at the Edge
&lt;/h2&gt;

&lt;p&gt;Before we finish up today, there’s one more thing that I want to mention. If you’re an &lt;a href="https://www.akamai.com/"&gt;Akamai&lt;/a&gt; customer, you actually have access to a malware protection feature as part of the web application firewall products. I got to play around with it briefly and want to show it off because it’s super cool.&lt;/p&gt;

&lt;p&gt;I have an application up and running at &lt;a href="http://uploader.austingil.com"&gt;uploader.austingil.com&lt;/a&gt;. It’s already integrated with &lt;a href="https://www.akamai.com/products/web-performance-optimization"&gt;Akamai’s Ion&lt;/a&gt; CDN, so it was easy to also set it up with a &lt;a href="https://www.akamai.com/products/app-and-api-protector"&gt;security configuration&lt;/a&gt; that includes IP/Geo Firewall, Denial of Service protection, WAF, and Malware Protection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--44zxBhzp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-85-1080x588.png%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--44zxBhzp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-85-1080x588.png%2520align%3D%2522left%2522" alt="" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I configured the Malware Protection policy to just deny any request containing malware or a content type mismatch.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LFayzJj6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-78-1080x487.png%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LFayzJj6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-78-1080x487.png%2520align%3D%2522left%2522" alt="" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, if I go to my application and try to upload a file that has known malware, I’ll see almost immediately the response is rejected with a 403 status code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--l0ct-rNm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-84-1080x581.png%2520align%3D%2522left%2522" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--l0ct-rNm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://austingil.com/wp-content/uploads/image-84-1080x581.png%2520align%3D%2522left%2522" alt="" width="" height=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To be clear, that’s logic I didn’t actually write into my application. That’s happening thanks to Akamai’s malware protection, and I really like this product for a number of reasons.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It’s convenient and easy to set up and modify from within the Akamai UI.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;I love that I don’t have to modify my application to integrate the product.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It does its job well and I don’t have to manage maintenance on it.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Last, but not least, the files are scanned on Akamai’s edge servers, which means it’s not only faster, but it also keeps blocked malware from ever even reaching my servers. This is probably my favorite feature.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Due to time and resource restrictions, I think Malware Protection can only scan files up to a certain size, so it won’t work for everything, but it’s a great addition for blocking some files from even getting into your system.&lt;/p&gt;

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

&lt;p&gt;It’s important to remember that there is no one-and-done solution when it comes to security. Each of the steps we covered have their own pros and cons, and it’s generally a good idea to add multiple layers of security to your application.&lt;/p&gt;

&lt;p&gt;Okay, that’s going to wrap up this series on file uploads for the web. If you haven’t yet, consider reading some of the other articles.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/uploading-files-with-html/"&gt;&lt;strong&gt;Upload files with HTML&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/upload-files-with-javascript/"&gt;&lt;strong&gt;Upload files with JavaScript&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/file-uploads-in-node/"&gt;&lt;strong&gt;Receive uploads in Node.js (Nuxt.js)&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/upload-to-s3/"&gt;&lt;strong&gt;Optimize storage costs with Object Storage&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/file-uploads-cdn/"&gt;&lt;strong&gt;Optimize performance with a CDN&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://austingil.com/file-upload-security-and-malware-protection/"&gt;&lt;strong&gt;Upload security &amp;amp; malware protection&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Please let me know if you found this useful, or if you have ideas on other series you’d like me to cover. I’d love to hear from you.&lt;/p&gt;

&lt;p&gt;Thank you so much for reading. If you liked this article, and want to support me, the best ways to do so are to &lt;a href="https://twitter.com/share?via=heyAustinGil"&gt;share it&lt;/a&gt;, &lt;a href="https://austingil.com/newsletter/"&gt;sign up for my newsletter&lt;/a&gt;, and &lt;a href="https://twitter.com/heyAustinGil"&gt;follow me on Twitter&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published on&lt;/em&gt; &lt;a href="http://austingil.com"&gt;&lt;em&gt;austingil.com&lt;/em&gt;&lt;/a&gt;&lt;em&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
