<?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: Digital Canvas Development</title>
    <description>The latest articles on DEV Community by Digital Canvas Development (@digitalcanvas-dev).</description>
    <link>https://dev.to/digitalcanvas-dev</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%2Forganization%2Fprofile_image%2F7132%2F121229d1-6939-453a-8d10-dd924002ac37.jpg</url>
      <title>DEV Community: Digital Canvas Development</title>
      <link>https://dev.to/digitalcanvas-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/digitalcanvas-dev"/>
    <language>en</language>
    <item>
      <title>Generative AI for 3D Modeling and Printing</title>
      <dc:creator>Simon Goldin</dc:creator>
      <pubDate>Fri, 07 Jul 2023 19:00:17 +0000</pubDate>
      <link>https://dev.to/digitalcanvas-dev/generative-ai-for-3d-modeling-and-printing-485b</link>
      <guid>https://dev.to/digitalcanvas-dev/generative-ai-for-3d-modeling-and-printing-485b</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;While exploring generative AI, I've seen what it can do with different web programming languages like JavaScript and I thought it might be interesting to see what it can do with 3d modeling.&lt;/p&gt;

&lt;p&gt;One tool many people use for parametric modeling is &lt;a href="https://openscad.org/" rel="noopener noreferrer"&gt;OpenSCAD&lt;/a&gt; which is very utilitarian and has methods for constructing 3d and 2d geometry (and 3d from 2d such as extrusion).&lt;/p&gt;

&lt;p&gt;Since "OpenSCAD focuses on the CAD aspects rather than the artistic aspects of 3D modeling" it's great for generating practical and functional 3d models (and also things like fractals, which might be a future topic), and I wanted to explore how chatGPT can help - in cutting down on time, exploring different designs, or at the very least teaching me new things.&lt;/p&gt;

&lt;p&gt;The project I decided on was a &lt;strong&gt;business card holder&lt;/strong&gt; - it's something that has a little bit of complexity, can lend itself to creativity, and is also relatively geometric and inorganic; not to mention it's something that I can use!&lt;/p&gt;

&lt;p&gt;For a little inspiration, I turned to MidJourney:&lt;/p&gt;

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

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

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

&lt;h2&gt;
  
  
  Initial Attempt
&lt;/h2&gt;

&lt;p&gt;First, I wanted to see what GPT-4 would give me out of the box, without much direction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;generate openSCAD code for a standard size business card holder&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Produces the following code (for the brevity, the "fluff" around it is omitted"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cardWidth = 90; 
cardHeight = 55; 
cardThickness = 5; 

boxWallThickness = 3; 
boxBottomThickness = 3; 
boxInsideWidth = cardWidth - 2; 
boxInsideHeight = cardHeight - 2; 
boxInsideDepth = cardThickness + boxBottomThickness; 

module box() {
    difference() {
        // Outer box
        cube([cardWidth, cardHeight, cardThickness]);
        // Inner box
        translate([boxWallThickness, boxWallThickness, boxBottomThickness])
        cube([boxInsideWidth, boxInsideHeight, boxInsideDepth]);
    }
}

box();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code generates two "cubes" (well, rectangular prisms) - one representing a stack of business cards, and larger one - and subtracts one from the other.&lt;/p&gt;

&lt;p&gt;This code is valid and renders the following shape (I added colors to illustrate the parts):&lt;/p&gt;

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

&lt;p&gt;The general model is right, but there are a few problems with this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The negative space is a bit too small - a standard business card is 88.9mm x 50.8mm but the cutout is 87mm long, so there will be an overhang.&lt;/li&gt;
&lt;li&gt;The depth of this container doesn't allow for many business cards.&lt;/li&gt;
&lt;li&gt;I am &lt;em&gt;pretty sure&lt;/em&gt; that chatGPT was "trying" to generate a box with walls on each side, but ended up completely removing two of them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We can play around with some of the variables to give us something more functional, or we can ask chatGPT do it for us:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;adjust the model so that there are four walls and a "base" and that a stack of standard business cards fits in the cutout.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Gives us:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cardWidth = 90; 
cardHeight = 55; 
stackThickness = 10; 

boxWallThickness = 3; 
boxBaseThickness = 3; 
boxInsideWidth = cardWidth + 2; 
boxInsideHeight = cardHeight + 2; 
boxInsideDepth = stackThickness + boxBaseThickness;

module box() {
    difference() {
        // Outer box
        cube([boxInsideWidth + (2 * boxWallThickness), boxInsideHeight + (2 * boxWallThickness), boxInsideDepth]);
        // Inner box
        translate([boxWallThickness, boxWallThickness, boxBaseThickness])
        cube([boxInsideWidth, boxInsideHeight, stackThickness]);
    }
}

box();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This results in a cutout that is 92mm x 57mm and 10mm high which gives us a little clearance for the business cards:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuaap7xh5ubu2fis0rxaz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuaap7xh5ubu2fis0rxaz.png" alt="business card box 3d model from complex code" width="800" height="539"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code could be cleaned up (for example by starting off with the dimensions of the entire stack of cards, and going from there) but the result is a perfectly functional container.&lt;/p&gt;

&lt;h4&gt;
  
  
  Another Approach
&lt;/h4&gt;

&lt;p&gt;We can take a more "engineering" approach by crafting our prompt as if we were writing the code ourselves. This might be less error-prone if the prompt is crafted well:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;generate openSCAD code for the following:&lt;/p&gt;

&lt;p&gt;a rectangular prism that is 98mm across, 63mm deep, and 13mm high.&lt;/p&gt;

&lt;p&gt;subtract a prism that is 92mm x 57mm and 10mm high from the top center of the previous prism.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Generates a very simple block of code (and goes on to explain what it does):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;difference() {
    cube([98, 63, 13]); // Outer rectangle
    translate([3, 3, 3]) cube([92, 57, 10]); // Inner rectangle subtracted from the outer rectangle
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And looks identical to the previous version:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpsgzxueyzjeqrpkju1by.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpsgzxueyzjeqrpkju1by.png" alt="business card box 3d model from simple code" width="800" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Tradeoffs
&lt;/h4&gt;

&lt;p&gt;The second approach resulted in much less code (generally good!) which was also more straightforward, but we had to do a little more "manual" work like finding the exact dimensions we need and breaking the prompt up into steps, as if we were writing the code ourselves.&lt;/p&gt;

&lt;p&gt;The first approach might still be better for exploration and experimentation, if we want to see wildly different versions of what a "business card holder" can look like.&lt;/p&gt;

&lt;p&gt;Either way, we get the same model which can be 3d-printed in a little under two hours:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hmv95ez0e6z6lh3dcvm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3hmv95ez0e6z6lh3dcvm.png" alt="screenshot of model imported and sliced in PrusaSlicer" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Going Further
&lt;/h2&gt;

&lt;p&gt;Let's combine these approaches to create a more interesting design.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generate openSCAD code for standard size business card holder composed of the following modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a base that is flat against the surface it will be placed.&lt;/li&gt;
&lt;li&gt;a small front face "lip" connected to the base that keeps the cards from slipping out.&lt;/li&gt;
&lt;li&gt;a back that has a bottom side that connects to the back side of the "base". This "back" shape is almost vertical but with the top rotated away from the front.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Color-code each module.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Skipping the code here, this renders:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvgpee94j1l4ybzx0tj1e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvgpee94j1l4ybzx0tj1e.png" alt="openSCAD screenshot of model generated from above code" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Obviously, that's not how we want the "back" oriented.&lt;/p&gt;

&lt;p&gt;Another attempt:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generate openSCAD code for standard size business card holder composed of the following modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a base that is flat against the surface it will be placed.&lt;/li&gt;
&lt;li&gt;a small front face "lip" connected to the base that keeps the cards from slipping out.&lt;/li&gt;
&lt;li&gt;a back that has a bottom side that connects to the back side of the "base". This "back" shape is almost vertical but rotated so that this "back" shape is tilted back, away from the front of the model.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Color-code each module.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3whbskl2578xp25y5qqt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3whbskl2578xp25y5qqt.png" alt="another attempt; back section is angled the wrong way" width="800" height="567"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Regenerating the response strays further away from what we want:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffz6p9t6rkalo8fktpzcf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffz6p9t6rkalo8fktpzcf.png" alt="attempted model with parts not aligned or rotated properly" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Further generations aren't much better, so we'll be direct when discussing these actions by giving it specific dimensions:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Generate openSCAD code for standard size business card holder composed of the following modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a 2mm thick, 90mm across, and 20mm deep bottom that is horizontal to the surface it will be placed on.&lt;/li&gt;
&lt;li&gt;a 2mm thick and 5mm tall front face "lip" at the top front of the base that will keep the cards from slipping out.&lt;/li&gt;
&lt;li&gt;a 2mm thick and 40mm tall vertical back side. Cards will rest on this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Rotate and translate each module to adhere to the description.&lt;/p&gt;

&lt;p&gt;Color-code each module for debugging. Do not provide anything except for the code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Does pretty much we want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$fn = 100; // Higher resolution

module base() {
    color("Blue")
    cube([90, 20, 2]);
}

module lip() {
    color("Green")
    translate([0, 15, 0])
    cube([90, 2, 5]);
}

module back() {
    color("Red")
    translate([0, 0, 2])
    cube([90, 2, 40]);
}

// Combine modules
base();
lip();
back();

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Rotating the back part and placing it correctly took some trial and error: &lt;code&gt;rotate the "back" 10 degrees so that the top is angled away from the front and move the "back" module so that the bottom edge connects to the back edge of the base.&lt;/code&gt; ... &lt;code&gt;fix the rotation direction&lt;/code&gt; ... &lt;code&gt;fix the rotation direction and placement so that the back is at the back, away from the lip&lt;/code&gt; ... &lt;code&gt;move the back piece to the back edge of the base.&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Eventually, I got more or less what I wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$fn = 100; // Higher resolution

module base() {
    color("Blue")
    cube([90, 20, 2]);
}

module lip() {
    color("Green")
    translate([0, 15, 0])
    cube([90, 2, 5]);
}

module back() {
    color("Red")
    translate([45, 0, 1]) // Move upwards by 1mm and center for rotation
    rotate([10, 0, 0]) // Rotate 10 degrees
    translate([-45, 0, 0]) // Re-center after rotation
    cube([90, 2, 40]);
}

// Combine modules
base();
lip();
back();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjoc8v8yugbdm7s0cyoig.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fjoc8v8yugbdm7s0cyoig.png" alt="simple business card holder with front lip, base, and tilted back" width="800" height="520"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After this point, I prompted GPT-4 to add "caps" to the sides:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;create two quadrilaterals that will act as left and right "caps" described as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;each cap is 2mm wide&lt;/li&gt;
&lt;li&gt;bottom edges are flush with the bottom of the base&lt;/li&gt;
&lt;li&gt;front edges are flush with the front lip&lt;/li&gt;
&lt;li&gt;back edges are flush with the back part&lt;/li&gt;
&lt;li&gt;top edges connect between the middle of the back part and the top of the front lip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best way would probably be to create the points and then extrude and then move them to different positions based on "left" or "right"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Which produced a syntactical error and not what I was going for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module cap(right) {
    color("Yellow")
    points = [[0,0],[90,0],[90,15],[45,35]]; // Define points of a polygon
    linear_extrude(height = 2)
        polygon(points=points); // Create the polygon
    translate([right ? 88 : 0, 0, 0]); // Position the cap
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;But it was on the right track, programmatically-speaking.&lt;/p&gt;

&lt;p&gt;Given that one of my initial goals was to &lt;em&gt;speed up&lt;/em&gt; development, I took the reigns and fixed the issues manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module cap(right) {
    color("Yellow");
    points = [[0, 0], [24, -4], [4, 16], [0, 16]]; // Points on the X-Y plane
    translate([right ? 90 : 2, 0, 0]) 
    rotate([0, -90, 0]) // Rotate points to Y-Z plane
    linear_extrude(height = 2) 
    polygon(points = points);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and fed it back to chatGPT.&lt;/p&gt;

&lt;p&gt;After this point, I asked chatGPT to increase the width from 90mm to 95mm to account for the end caps, "etched" text into the back, and then made some manual adjustments to arrive at my final (for now) product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module base() {
    color("Blue")
    cube([95, 20, 2]);
}

module lip() {
    color("Green")
    translate([0, 18, 0])
    cube([95, 2, 5]);
}

module back() {
    color("Red")
    difference() {
        translate([0, 0, 1]) // Move upwards by 1mm and center for rotation
        rotate([10, 0, 0]) // Rotate 10 degrees
        cube([95, 2, 40]);
        translate([85.5, -2, 18]) // Adjusted position of the text
        scale([.65, 1, .65]) // Decrease scale of the text
        rotate([260, 180, 0]) // Adjusted rotation to make the text parallel with the 'back'
        linear_extrude(height = 2, convexity = 2)
        text("digitalcanvas.dev", font = "Merriweather"); // Text to be cut out
    }
}

module cap(right) {
    color("Yellow");
    points = [[0, 0], [24, -4], [5, 18], [0, 18]]; // Points on the X-Y plane
    translate([right ? 95 : 2, 0, 0]) 
    rotate([0, -90, 0]) // Rotate points to Y-Z plane
    linear_extrude(height = 2) 
    polygon(points = points);
}

// Combine modules
base();
lip();
back();
cap(true); // Right cap
cap(false); // Left cap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;I loaded this into my slicer (&lt;a href="//www.prusa3d.com/prusaslicer/"&gt;PrusaSlicer&lt;/a&gt;) and began the print!&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Final thoughts &amp;amp; takeaways
&lt;/h2&gt;

&lt;p&gt;Ultimately, I didn't get the level of polish that MidJourney teased was possible, and the process was imperfect but a good learning experience for both working with Generative AI and OpenSCAD. &lt;/p&gt;

&lt;p&gt;GPT-4 gave valid code the vast majority of the time and I was able to adjust it when needed. Having larger chunks of code generated for me definitely saved time - both in typing and looking up documentation - and being able to tweak specific numbers and feed it back to chatGPT allowed for a relatively smooth workflow; though I concede that as a first try, using chatGPT was slower than coding it by hand; I spent a lot of time checking and tweaking the generated outputs (not to mention having to wait for rate limits to expire).&lt;/p&gt;

&lt;p&gt;There were some themes that came out of this. Generated OpenSCAD code was prone to mixing up axes when rotating and translating, and getting the right "Points on the X-Y plane" was a struggle. Doing this manually was much faster, but prompts like "the module was rotated along the wrong axis" usually worked, too.&lt;/p&gt;

&lt;p&gt;It's also important to be as direct as possible - do not assume "module A should connect to module B" will result in what you expect; give more direction: "the bottom edge of module A should be flush with the top edge of module B and the smaller edge should be centered on the larger edge." &lt;/p&gt;

&lt;p&gt;Finally, it helps to break the end goal into smaller tasks (e.g. "generate module A", "adjust model A", "add module B") rather than start with a larger prompt that has more things that can go wrong. Interestingly, &lt;em&gt;generating&lt;/em&gt; modules was generally less error-prone than &lt;em&gt;modifying&lt;/em&gt; them.&lt;/p&gt;

&lt;p&gt;In my opinion, it's best to treat it as pair programming where you hand the work off between two software engineers while speaking in "&lt;a href="https://www.agilealliance.org/resources/experience-reports/mob-programming-agile2014/" rel="noopener noreferrer"&gt;the highest level of abstraction&lt;/a&gt;" (which, in some cases, is lower than you'd think).&lt;/p&gt;

&lt;p&gt;Thank you for reading! Have you used generative AI for 3d modeling or printing? What approach worked well for you? I would love to hear about other experiences.&lt;/p&gt;

&lt;p&gt;Of course, I can't leave this post unfinished! Here is the final product:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwzwc1p8qr2rkameu839.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwzwc1p8qr2rkameu839.jpg" alt="photo of final 3d-printed product" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The real business cards are in the mail, but I printed a fake one!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi6fgrsqk7pm6kof0fdpn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi6fgrsqk7pm6kof0fdpn.jpg" alt="photo of final 3d-printed product with a business card placeholder" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>3dprinting</category>
      <category>beginners</category>
      <category>programming</category>
    </item>
    <item>
      <title>AI/LLM Recipe Generator with chatGPT</title>
      <dc:creator>Simon Goldin</dc:creator>
      <pubDate>Mon, 26 Jun 2023 19:28:27 +0000</pubDate>
      <link>https://dev.to/digitalcanvas-dev/aillm-recipe-generator-with-chatgpt-4dnk</link>
      <guid>https://dev.to/digitalcanvas-dev/aillm-recipe-generator-with-chatgpt-4dnk</guid>
      <description>&lt;p&gt;The ChatGPT API is like a magic spell for your web application - with just a few lines of code, it can conjure up engaging, intelligent conversations. Even for a tech novice, it's a breeze to weave into new or existing apps. Dive in, and in no time, you'll have a conversational AI that keeps users captivated and coming back for more.&lt;/p&gt;




&lt;p&gt;That's the introduction that chatGPT came up with. Pretty good, right?&lt;/p&gt;

&lt;p&gt;In this article I won't be building a conversational AI tool, but I will go into the integration between a &lt;a href="https://remix.run/" rel="noopener noreferrer"&gt;Remix&lt;/a&gt; application and the chatGPT API.&lt;/p&gt;

&lt;p&gt;The "test bed" will be a simple recipe generator that gets some information from the user that it will use to create a prompt for chatGPT.&lt;/p&gt;

&lt;p&gt;The code is available &lt;a href="https://github.com/digital-canvas-dev/ai-recipe-generator" rel="noopener noreferrer"&gt;on github&lt;/a&gt; and ultimately looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8k7o4ld0l0m0h4gchi2w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8k7o4ld0l0m0h4gchi2w.png" alt="screenshot of what this app might look like" width="800" height="1242"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-setup
&lt;/h2&gt;

&lt;p&gt;The "pre-setup" is as straightforward as can be. (After setting up payment) you will need to create a secret API key &lt;a href="https://platform.openai.com/account/api-keys" rel="noopener noreferrer"&gt;here&lt;/a&gt; and copy it to your &lt;code&gt;.env&lt;/code&gt; file (make sure it's in your .gitignore file so no one can find it on github!). Also copy your organization ID from &lt;a href="https://platform.openai.com/account/org-settings" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OPENAI_API_KEY=[your secret API key]
OPENAI_ORG_KEY=[your organization id]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And install the &lt;a href="https://github.com/openai/openai-node" rel="noopener noreferrer"&gt;&lt;code&gt;openai&lt;/code&gt; library&lt;/a&gt;. If using &lt;code&gt;npm&lt;/code&gt;, that would be:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;This includes TypeScript types, too!&lt;/p&gt;

&lt;h2&gt;
  
  
  Calling the API
&lt;/h2&gt;

&lt;p&gt;For this example, we can use the &lt;a href="https://platform.openai.com/docs/guides/gpt/chat-completions-api" rel="noopener noreferrer"&gt;Chat Completions API&lt;/a&gt;, but before we can do that, we'll need to configure the library to use our keys. Since this code is exclusively run on a server, and not a user's browser, we can get what we need from the &lt;code&gt;process.env&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;configuration&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;Configuration&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="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="na"&gt;organization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OPENAI_API_ORG&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;openai&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;OpenAIApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can create an object of type &lt;code&gt;CreateChatCompletionRequest&lt;/code&gt;. This object can have a variety of different options to tell chatGPT what we want, but the most important (and required) options are &lt;code&gt;model&lt;/code&gt; - which model version to use (full list &lt;a href="https://platform.openai.com/docs/models" rel="noopener noreferrer"&gt;here&lt;/a&gt;), and &lt;code&gt;messages&lt;/code&gt; - the context and prompt of the chat we want completed.&lt;/p&gt;

&lt;p&gt;One &lt;code&gt;messages&lt;/code&gt; option can be a system-wide "personality" that the chat can conform to. There are also options to give the API examples of outputs that can be used to train the model for this specific chat.&lt;/p&gt;

&lt;p&gt;We'll be using a &lt;code&gt;user&lt;/code&gt; message, i.e. the input received from the user, to ask the GTP model what we want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// for the purpose of this article, we'll abstract this away.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ingredientsList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getIngredientsList&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;completionRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CreateChatCompletionRequest&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="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;system&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;You are a creative and experienced chef assistant.&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="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="s2"&gt;`Generate a recipe with these ingredients: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ingredientsList&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="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;chatCompletion&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;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createChatCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;completionRequest&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, we're using the &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; model and starting the conversation by asking the API to act as a "creative and experienced chef assistant".&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling the response
&lt;/h2&gt;

&lt;p&gt;The response is well-typed and can be accessed easily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generatedOutput&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;chatCompletion&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;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;Which in this case might result in a "One-Pan Baked Tilapia and Vegetable Dinner" recipe with a complete ingredients list and step-by-step instructions!&lt;/p&gt;

&lt;p&gt;This is, of course, a simplification. A full implementation with more options and a user interface might end up looking something like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tbd8ihuhbzejrv33qbd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8tbd8ihuhbzejrv33qbd.png" alt="Full UI screenshot with inputs and generated output" width="800" height="2452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced options
&lt;/h2&gt;

&lt;p&gt;An interesting property that's available to us here, that's not available in the chatGPT interface is &lt;a href="https://platform.openai.com/docs/api-reference/completions/create#completions/create-temperature" rel="noopener noreferrer"&gt;&lt;code&gt;temperature&lt;/code&gt;&lt;/a&gt; which is an abstraction of randomness that can add some chaos.&lt;/p&gt;

&lt;p&gt;With a &lt;code&gt;temperature&lt;/code&gt; of 2, a "recipe" might start looking like this...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Chicken Delight Recipe Parham Style:

Featured Cooking Equipment(set boAirition above stove required Gas-Telian range VMM incorporated rather below ideal temperature during baking ir regulate heat applied):
- Large non-stick frypan(Qarma brand)-&amp;gt;Coloning cooking Stenor service(each Product hasown separate reviews dependable optimization features)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Be careful, as this can eat into your tokens! As a safety measure (or depending on your use case), a &lt;code&gt;max_tokens&lt;/code&gt; can be used to limit the size of the output.&lt;/p&gt;

&lt;p&gt;In my experience with gpt-3.5-turbo, altering the &lt;code&gt;system&lt;/code&gt; content did not have much effect in this case, but can be more useful for ongoing conversations. Since my use case is to just ask for a recipe once, there's no need to set up the system "personality".&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;As of this writing, &lt;code&gt;gpt-3.5-turbo&lt;/code&gt; is the latest model available to me but it comes with some limitations.&lt;/p&gt;

&lt;p&gt;First off, the processing is fairly slow with it taking about 15 seconds to return a recipe. OpenAI suggests a number of improvements in &lt;a href="https://platform.openai.com/docs/guides/production-best-practices/improving-latencies" rel="noopener noreferrer"&gt;their docs&lt;/a&gt; such as limiting output size, caching, and batching.&lt;/p&gt;

&lt;p&gt;There is also an inherent limitation that a conversation is "stateless": if you want to have an ongoing conversation, each previous &lt;code&gt;user&lt;/code&gt; message and its &lt;code&gt;assistant&lt;/code&gt; response need to be sent before each new &lt;code&gt;user&lt;/code&gt; message.&lt;/p&gt;

&lt;p&gt;In my example application, providing a very limited set of common ingredients (&lt;code&gt;Salt, Pepper, Olive oil, Butter, All-purpose flour, Sugar, Eggs, Milk, Garlic, Onion, Lemons, White Vinegar, Apple Cider Vinegar, Soy sauce, Baking powder, Cumin&lt;/code&gt;) still results in chicken- or shrimp-based recipes. I tried getting around this with more specific prompts ("if chicken is not available, do not recommend recipes with chicken.") but to not to much success.&lt;/p&gt;

&lt;p&gt;This is an example of a "hallucination" but has not specifically been an issue with GPT-4, which is not yet broadly available via the API.&lt;/p&gt;

&lt;p&gt;There are other important general generative AI limitations to keep in mind such as &lt;a href="https://www.forbes.com/sites/forbestechcouncil/2023/03/31/uncovering-the-different-types-of-chatgpt-bias/" rel="noopener noreferrer"&gt;biases&lt;/a&gt; and how they are often "confidently incorrect".&lt;/p&gt;

&lt;p&gt;In this case, the worst-case scenario is an unappealing meal, but these limitations are important to keep in mind when relying on generated content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fine-tuning and cost
&lt;/h2&gt;

&lt;p&gt;As the only user of this application, my costs have been very minimal 😅. A single execution comes out to about 200 input tokens and the output ranges between 300 and 500 tokens. With  &lt;code&gt;gpt-3.5-turbo&lt;/code&gt;, this comes out to &lt;code&gt;(0.2 * $0.0015) + (.4 * $0.002)&lt;/code&gt; or about &lt;em&gt;one tenth of a cent&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Once more generally available, GPT-4 will be significantly more expensive. Currently, a single run for me would amount to &lt;code&gt;(0.2 * $0.03) + (0.4 * $0.06)&lt;/code&gt; or about &lt;em&gt;3 cents&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;API pricing was &lt;a href="https://openai.com/blog/function-calling-and-other-api-updates" rel="noopener noreferrer"&gt;reduced a few weeks ago&lt;/a&gt; so it's reasonable to expect GPT-4 to get cheaper in the future, too. &lt;/p&gt;

&lt;p&gt;The GPT-3.5 output can still be fine-tuned by more specific and verbose inputs, but since billing is based on number of "tokens" (i.e. input and output length), fine-tuning this way can be costly, similarly to a conversational application that chains user and assistant messages.&lt;/p&gt;

&lt;p&gt;Prompts can also be split into smaller, more specific prompts. However, in addition to increasing the total number of tokens, this approach would also increase the complexity (and maintenance cost) of an application, especially if you're using the output of one query as an input of another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The header image for this post was created with MidJourney, just another (tiny) example of how I've been using the technology.&lt;/p&gt;

&lt;p&gt;Generative AI opens up a wide range of new and exciting applications, but not without additional considerations that should be kept in mind.&lt;/p&gt;

&lt;p&gt;Though this application just barely scratches the surface of what can be done, I hope it has served as a useful introduction to integrating the &lt;code&gt;openai&lt;/code&gt; library into your web application whether you're building a cool product, or just exploring new technologies.&lt;/p&gt;

&lt;p&gt;Have you explored interesting applications of the API, or experimented with different parts of it? Please share!&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>ai</category>
    </item>
    <item>
      <title>Serverless Remix App Contact Form with AWS Lambda, AWS SES and Google ReCaptcha</title>
      <dc:creator>Simon Goldin</dc:creator>
      <pubDate>Mon, 19 Jun 2023 16:11:41 +0000</pubDate>
      <link>https://dev.to/digitalcanvas-dev/serverless-remix-app-contact-form-with-aws-lambda-aws-ses-and-google-recaptcha-25dn</link>
      <guid>https://dev.to/digitalcanvas-dev/serverless-remix-app-contact-form-with-aws-lambda-aws-ses-and-google-recaptcha-25dn</guid>
      <description>&lt;h1&gt;
  
  
  Introduction
&lt;/h1&gt;

&lt;p&gt;This blog post revisits my (apparent) "Check out this new React framework" series that I've strayed away from.&lt;/p&gt;

&lt;p&gt;Specifically, I'm going to go through a contact form implementation adapted from working on the &lt;a href="https://digitalcanvas.dev" rel="noopener noreferrer"&gt;Digital Canvas Development website&lt;/a&gt; which is built on top of the Remix "&lt;a href="https://github.com/remix-run/grunge-stack" rel="noopener noreferrer"&gt;Grunge Stack&lt;/a&gt;".&lt;/p&gt;

&lt;p&gt;The entire website, including the specifics covered here, are available on github: &lt;a href="https://github.com/digital-canvas-dev/digitalcanvas.dev" rel="noopener noreferrer"&gt;https://github.com/digital-canvas-dev/digitalcanvas.dev&lt;/a&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Project overview
&lt;/h1&gt;

&lt;p&gt;My application is a simple site that will act as a landing page for my new business. It's mainly composed of static content and a contact form.&lt;/p&gt;

&lt;p&gt;Remix is a full-stack framework that co-locates server-side and client-side code. The Grunge Stack comes with a host of features and libraries including AWS deployment via &lt;a href="https://arc.codes/" rel="noopener noreferrer"&gt;Architect&lt;/a&gt; (e.g. &lt;code&gt;npx arc&lt;/code&gt;). Many of the AWS tools are free-tier eligible for low-traffic sites.&lt;/p&gt;

&lt;p&gt;This guide assumes you've already deployed your application or you're ready to.&lt;/p&gt;

&lt;p&gt;To get the contact form working end-to-end, I'll be using AWS Simple Email Service (SES). To prevent spam, I will be using Google ReCaptcha (v2).&lt;/p&gt;

&lt;h1&gt;
  
  
  Component setup
&lt;/h1&gt;

&lt;p&gt;The initial form looks very similar to form portion of the official &lt;a href="https://remix.run/docs/en/main/tutorials/blog#actions" rel="noopener noreferrer"&gt;Remix "blog" tutorial&lt;/a&gt;. In a nutshell, we're starting with something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;Form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useActionData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useSubmit&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;@remix-run/react&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;InputText&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;my-component-library&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;Contact&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;actionData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useActionData&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;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useSubmit&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;onSubmit&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;e&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;await&lt;/span&gt; &lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Form&lt;/span&gt; &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt; &lt;span class="na"&gt;onSubmit&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;onSubmit&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;InputText&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"name"&lt;/span&gt;
          &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Name"&lt;/span&gt;
          &lt;span class="na"&gt;errorFeedback&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;actionData&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... other fields ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Send&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Form&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;section&lt;/span&gt;&lt;span class="p"&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;And the core of our &lt;code&gt;action&lt;/code&gt; looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&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="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// we'll pseudocode this away.&lt;/span&gt;
  &lt;span class="c1"&gt;// it will return an object of errors, if any.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;validate&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="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// todo: avoid spam&lt;/span&gt;
  &lt;span class="c1"&gt;// todo: send the email&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;success&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;successMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Message sent! Expect to hear back soon.&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;You'll notice two important &lt;code&gt;todo&lt;/code&gt;s which we'll be addressing in reverse order so we can tackle the more interesting parts first 😀&lt;/p&gt;

&lt;h1&gt;
  
  
  Sending emails with AWS Simple Email Service (SES)
&lt;/h1&gt;

&lt;p&gt;To integrate with SES, I pulled in the AWS SDK (v3):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @aws-sdk/client-ses
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@aws-sdk/client-ses&lt;/code&gt; is a wrapper around AWS SES v3 and its documentation is &lt;a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-ses/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;**Important note: your lambda function must use nodeJS v18 to use the AWS v3 SDK. Otherwise, you'll need to use the v2 SDK.&lt;/p&gt;

&lt;p&gt;One of the cool thing about using AWS for email architecture with the Grunge Stack is that as long as you already followed the &lt;a href="https://github.com/remix-run/grunge-stack#deployment" rel="noopener noreferrer"&gt;deployment steps&lt;/a&gt;, the &lt;code&gt;client-ses&lt;/code&gt; library will be able to use the AWS keys from the environment variables that were set in the &lt;a href="https://github.com/remix-run/grunge-stack/blob/c64891d437a6d82a9c17ffbd3b70efe22c931fc1/.github/workflows/deploy.yml#L160-L161" rel="noopener noreferrer"&gt;.deploy script&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now (after looking at a lot of interfaces and documentation) the &lt;code&gt;action&lt;/code&gt; implementation becomes clear: we can simply create an instance of the SESClient, and use it to send an email!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture note:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We can break these methods into a few logical (and reusable, and testable) parts.&lt;/p&gt;

&lt;p&gt;This way, we can maintain the instantiation in one place and reuse it later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Putting it all together, the logic might look like this...&lt;/p&gt;

&lt;p&gt;A server-only &lt;code&gt;ses.server.ts&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ActionArgs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@remix-run/node&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;SendEmailCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SendEmailCommandInput&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;SESClient&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;@aws-sdk/client-ses&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;sendEmail&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;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SendEmailCommandInput&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;sesClient&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;SESClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&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;command&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;SendEmailCommand&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;sesClient&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="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And an updated action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;action&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;request&lt;/span&gt; &lt;span class="p"&gt;}:&lt;/span&gt; &lt;span class="nx"&gt;ActionArgs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;success&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;successMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;success&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;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;form&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;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;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;formData&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;requesterName&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;name&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;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;Source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;no-reply@...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;ToAddresses&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;...&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;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Form submission&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;Html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Someone submitted the contact form: Name &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requesterName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, Email: ...`&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&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;sendEmail&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sentError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;httpStatusCode&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;form&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 sending email.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sentError&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;success&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;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sentError&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="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;success&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;successMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Thank you for reaching out! Expect to hear back soon.&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;At this point, you can enable SES and finish the AWS set up as described &lt;a href="https://repost.aws/knowledge-center/lambda-send-email-ses" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once your domain is verified and set up is complete, you can send a test email from your dev environment!&lt;/p&gt;

&lt;h1&gt;
  
  
  Preventing spam with Google ReCaptcha v2
&lt;/h1&gt;

&lt;p&gt;However, deploying at this point is risky. Even if you're only sending emails to your own email address, a malicious actor might try to continuously submit your form which can rate-limit you or rack up your AWS bill.&lt;/p&gt;

&lt;p&gt;In lieu of (or in addition to) site-wide spam prevention, we'll make sure that the form is submitted by a person by adding a checkbox captcha. I decided not to use v3, because frankly, I didn't like the "protected by reCAPTCHA" badge in the bottom-right corner.&lt;/p&gt;

&lt;p&gt;First, you'll need to create a ReCaptcha &lt;a href="https://www.google.com/u/0/recaptcha/admin/" rel="noopener noreferrer"&gt;here&lt;/a&gt;. Fill in a label, select &lt;code&gt;Challenge (v2)&lt;/code&gt; and &lt;code&gt;"I'm not a robot" Checkbox&lt;/code&gt; and add the domain name the captcha will be on (while you're here, it's a good idea to create one for &lt;code&gt;localhost&lt;/code&gt; and another for a staging environment, if you're using it).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhne3o9w2shn97qvzfrih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhne3o9w2shn97qvzfrih.png" alt="example of a new recaptcha being created" width="630" height="445"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll be presented with "site key" and "secret key" that you can add to your 1) &lt;code&gt;.env&lt;/code&gt; file, and 2) github repo settings.&lt;/p&gt;

&lt;p&gt;I named them &lt;code&gt;CAPTCHA_SITE_KEY&lt;/code&gt; and &lt;code&gt;CAPTCHA_SECRET&lt;/code&gt; respectively.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr24y39yz1v0w3du4y7wn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr24y39yz1v0w3du4y7wn.png" alt="example of captcha values being added to github environment secrets and variables settings page" width="795" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The site key will link the site where the form is to ReCaptcha, and the secret key will only be used server-side to validate the value generated by ReCaptcha in the browser.&lt;/p&gt;

&lt;p&gt;Next, we'll install the &lt;a href="https://github.com/dozoisch/react-google-recaptcha" rel="noopener noreferrer"&gt;&lt;code&gt;react-google-recaptcha&lt;/code&gt; library&lt;/a&gt; with &lt;code&gt;npm i react-google-recaptcha&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Import it on the page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import ReCAPTCHA from 'react-google-recaptcha';
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and render it in the component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;export const Contact = () =&amp;gt; {
&lt;/span&gt;&lt;span class="gi"&gt;+ const [recaptchaValue, setRecaptchaValue] = useState&amp;lt;string | null&amp;gt;(null);
+ const recaptchaRef = useRef&amp;lt;ReCAPTCHA&amp;gt;(null);
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  const actionData = useActionData();
&lt;span class="err"&gt;
&lt;/span&gt;  const submit = useSubmit();
&lt;span class="err"&gt;
&lt;/span&gt;  const onSubmit = async (e) =&amp;gt; {
    await submit(e.currentTarget);
&lt;span class="gi"&gt;+   setRecaptchaValue(null);
+   recaptchaRef?.current?.reset();
&lt;/span&gt;  };
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ const handleRecaptchaChange = (value: string | null) =&amp;gt; {
+   setRecaptchaValue(value);
+ };
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  return (
    &amp;lt;section&amp;gt;
      &amp;lt;Form method='POST' onSubmit={onSubmit}&amp;gt;
        &amp;lt;InputText
          name='name'
          label='Name'
          errorFeedback={actionData?.errors?.name ?? null}
        /&amp;gt;
        {/* ... other fields ... */}
&lt;span class="gi"&gt;+       &amp;lt;input type='hidden' name='recaptchaValue' value={recaptchaValue} /&amp;gt;
+       &amp;lt;ReCAPTCHA
+         ref={recaptchaRef}
+         onChange={handleRecaptchaChange}
+       /&amp;gt;
&lt;/span&gt;        &amp;lt;button type='submit'&amp;gt;Send&amp;lt;/button&amp;gt;
      &amp;lt;/Form&amp;gt;
    &amp;lt;/section&amp;gt;
  );
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this to work, the &lt;code&gt;CAPTCHA_SITE_KEY&lt;/code&gt; needs to be accessible by the component, so we can use a loader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&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;loader&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="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;TypedResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Globals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CAPTCHA_SITE_KEY&lt;/span&gt;&lt;span class="dl"&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="o"&gt;&amp;gt;&amp;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="nx"&gt;json&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Pick&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Globals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CAPTCHA_SITE_KEY&lt;/span&gt;&lt;span class="dl"&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="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="na"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;CAPTCHA_SITE_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="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CAPTCHA_SITE_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="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and access the data from the component with &lt;code&gt;useLoaderData&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  const data = useLoaderData&amp;lt;{ ENV: Pick&amp;lt;Globals, 'CAPTCHA_SITE_KEY'&amp;gt; }&amp;gt;();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lastly (for the component), we'll fill in the real &lt;code&gt;sitekey&lt;/code&gt; prop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&amp;lt;ReCAPTCHA
  ref={recaptchaRef}
  onChange={handleRecaptchaChange}
&lt;span class="gi"&gt;+ sitekey={data.ENV.CAPTCHA_SITE_KEY}
&lt;/span&gt;/&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;We'll need to update the action again, to validate the ReCaptcha:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;export const action = async ({ request }: ActionArgs): Promise&amp;lt;{ success: true, successMessage: string; } | {
&lt;/span&gt;  success: false,
  errors: { form: string }
}&amp;gt; =&amp;gt; {
  const formData = await request.formData();
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ const recaptchaValue = formData.get('recaptchaValue');
+
+ const captchaResponse = await validateCaptcha(recaptchaValue);
+
+ if (!captchaResponse.success) {
+   return json({
+     success: false,
+     errors: {
+       recaptchaValue: 'Invalid ReCAPTCHA response.',
+     },
+   });
+ }
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  const requesterName = formData.get('name');
&lt;span class="err"&gt;
&lt;/span&gt;  // params, etc...
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can create the &lt;code&gt;validateCaptcha&lt;/code&gt; function and put it in a &lt;code&gt;captcha.server.ts&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ReCaptchaURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.google.com/recaptcha/api/siteverify&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;validateCaptcha&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;recaptchaValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;FormDataEntryValue&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="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;captchaResponse&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="nx"&gt;ReCaptchaURL&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/x-www-form-urlencoded&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="s2"&gt;`secret=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CAPTCHA_SECRET&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;response=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;recaptchaValue&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;captchaResponse&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;CAPTCHA_SECRET&lt;/code&gt; will be pulled from your build or .env file and there's nothing left to do!&lt;/p&gt;

&lt;p&gt;A complete implementation, with more fields and some styling might look like this (at least mine does, as of this writing!):&lt;/p&gt;

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

&lt;h1&gt;
  
  
  Outro
&lt;/h1&gt;

&lt;p&gt;Thank you for reading! Tying in these different SDKs into a new application touched on a lot of things I've done in the past, but not all together in a Remix application. As the web has progressed, developers have been given so many options and different (paid and often free) ways of doing things, and nearly infinite opportunities to learn new things.&lt;/p&gt;

&lt;p&gt;That being said, consider alternative 3rd party tools such as &lt;a href="https://mailchimp.com/" rel="noopener noreferrer"&gt;MailChimp&lt;/a&gt; or &lt;a href="https://sendgrid.com/" rel="noopener noreferrer"&gt;SendGrid&lt;/a&gt; which are more flexible (not tied to AWS and not part of the codebase) and more approachable (they can be configured without needing to code).&lt;/p&gt;

&lt;p&gt;Props to &lt;a href="https://reiland.dev/blog/serverless-contact-form-lambda/" rel="noopener noreferrer"&gt;this article&lt;/a&gt; that I found after building a working proof of concept which gave me some ideas for improvements (for example, I didn't know that &lt;code&gt;client-ses&lt;/code&gt; had built-in authentication and my first version was managing the AWS configuration manually!). Even 13 years into this, I'm glad that I'm always learning.&lt;/p&gt;

</description>
      <category>node</category>
      <category>remix</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
