<?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: Simon Goldin</title>
    <description>The latest articles on DEV Community by Simon Goldin (@goldins).</description>
    <link>https://dev.to/goldins</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%2F399367%2F907bf80d-5dd1-4ed6-8cc2-2fbe6c68130d.png</url>
      <title>DEV Community: Simon Goldin</title>
      <link>https://dev.to/goldins</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/goldins"/>
    <language>en</language>
    <item>
      <title>Trust But Verify: Evaluating Security When Choosing Data Infrastructure Tools</title>
      <dc:creator>Simon Goldin</dc:creator>
      <pubDate>Fri, 14 Mar 2025 14:26:41 +0000</pubDate>
      <link>https://dev.to/goldins/trust-but-verify-evaluating-security-when-choosing-data-infrastructure-tools-2cge</link>
      <guid>https://dev.to/goldins/trust-but-verify-evaluating-security-when-choosing-data-infrastructure-tools-2cge</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Cover Image: How data is stored on a server, as envisioned by Hackers (1995) Dir. Iain Softley ©Metro-Goldwyn-Mayer, Inc.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h6&gt;
  
  
  A data leader's guide to maintaining security when working with third-party vendors
&lt;/h6&gt;

&lt;h6&gt;
  
  
  Originally published on &lt;a href="https://blog.twingdata.com/p/trust-but-verify-evaluating-security" rel="noopener noreferrer"&gt;Twing Data&lt;/a&gt;
&lt;/h6&gt;

&lt;p&gt;Data warehouse optimization tools connect to platforms such as Snowflake, Amazon Redshift, and Google BigQuery to analyze usage and improve performance and cost. Since these tools interface with sensitive enterprise environments, robust security practices are critical.&lt;/p&gt;

&lt;p&gt;Below, we’ll dive into security aspects that you should consider when choosing warehouse optimization and observability tools for your organization. More generally, these guidelines can apply to other tools that access your infrastructure or data.&lt;/p&gt;

&lt;p&gt;Thanks for reading Twing Data! Subscribe for free to receive new posts and support my work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Access
&lt;/h2&gt;

&lt;p&gt;How can you be confident in the safety of the tool that's analyzing your warehouse? That's a tough question. By knowing what it can access and by what means, you can begin to form an answer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Least Privilege
&lt;/h3&gt;

&lt;p&gt;An important aspect of security is the “principle of least privilege”, or making sure that different services (especially tools that aren’t developed internally) can only access what they need to be effective. In a data mart set up, that can be a lot of critical information, so it’s important to minimize exposing anything that doesn’t need to be.&lt;/p&gt;

&lt;p&gt;Limiting an integration to a dedicated user is a good first step. Giving this user minimal permissions, such as read-only metadata-only access, also alleviates some risk as this user could be revoked with minimal disruption if needed.&lt;/p&gt;

&lt;p&gt;For more complex integrations, it may be necessary to have role-based access controls (RBAC) to manage who has access to what. For example, someone inspecting the metadata may not be the same person setting up billing or data connections, but maybe they should still be able to see what connections are set up. Some tools can go even further and support data-specific privilege and limit who can access different parts of the (meta)data and optimization and observability results themselves.&lt;/p&gt;

&lt;p&gt;Twing Data takes this principle seriously by supporting secure connections to your warehouses and limiting access to read-only metadata by design and following best practices internally.&lt;/p&gt;

&lt;h3&gt;
  
  
  API &amp;amp; Integration Security
&lt;/h3&gt;

&lt;p&gt;Different vendors have different, and usually multiple, ways to connect to their systems such as passwords, key-pair authentication, gateways, or entirely unique service accounts.&lt;/p&gt;

&lt;p&gt;It’s important to remember that encryption is only as strong as its implementation, and to verify that a more secure method actually is more secure. For example, simply switching from HTTP to HTTPS doesn’t guarantee security if certificate validation is improperly handled, allowing for man-in-the-middle attacks. Similarly, using OAuth 2.0 for API authentication is a step up from basic authentication, but failing to properly manage token expiration, refresh, and revocation could expose your integration to token hijacking or replay attacks. Always evaluate the entire security chain, not just the algorithm or protocol. We’ll talk more about encryption below.&lt;/p&gt;

&lt;p&gt;A quick internet search can also reveal any vulnerabilities that were exposed in the past for a given vendor or user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data Management
&lt;/h2&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%2Fjn5c5okwp6fyzwomknkb.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%2Fjn5c5okwp6fyzwomknkb.jpg" alt="Header image: person taking inventory in a warehouse" width="800" height="671"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Photo by Mauricio Gutiérrez on Unsplash&lt;/p&gt;

&lt;p&gt;Whether it’s just metadata like the queries being run or the data they return, whoever is analyzing the data needs to ingest and store it somewhere for analysis, and possibly to surface it back to their users. This section analyzes which aspects may require a closer look.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Retention
&lt;/h3&gt;

&lt;p&gt;Since the vendor may have access to business-critical data (or metadata) it’s important to consider their data retention policy. What a “good” policy is will vary from business to business and use case to use case, but typically you don’t want someone to hold on to your data “forever” (especially not past the point of canceling the service). Long-term data retention can introduce unnecessary risks, such as unauthorized access, data breaches, or compliance violations. On the flipside, too short of retention might give less useful insights.&lt;/p&gt;

&lt;p&gt;When vetting vendors, ask about their data retention and deletion policies up front and negotiate shorter retention periods or periodic purges, especially for sensitive information. It’s also a good idea to have an offboarding plan that verifies that the third-party has deleted your data after your integration has ended.&lt;/p&gt;

&lt;p&gt;While some vendors don’t share this policy, Twing Data retains 90 days of metadata and persists the resulting analysis during the duration of service. This can be adjusted on a company-by-company basis if requested.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Isolation
&lt;/h3&gt;

&lt;p&gt;There are also different methods for segmenting or isolating data and access. Separating customers’ data from one another can be done by setting up separate environments for each customer, ensuring that risk from one customer isn’t carried over to another. This can be especially important for services with privileges that extend beyond read-only, such as those that automatically adjust the size, clustering, or resources of your warehouses, or services that rely on data beyond metadata.&lt;/p&gt;

&lt;p&gt;Twing Data can safely grant low-level access to our users by only exposing our analyses through company-specific accounts and views, ensuring that users (or threat actors) aren’t able to view anything they’re not supposed to.&lt;/p&gt;

&lt;h3&gt;
  
  
  Data Anonymization
&lt;/h3&gt;

&lt;p&gt;Besides the tactics listed above, a vendor can, and should, anonymize sensitive data — even metadata. Even seemingly innocuous queries that may be executed can contain personal information that is not necessary for an external tool to see. Consider a query such as &lt;code&gt;“WHERE username = ‘celebrity@personalemail.com’ AND phonenumber = ‘(555) 867-5309’”&lt;/code&gt; being exposed. A leaked query such as this not only exposes a high-profile individual’s email address but also ties it to their phone number.&lt;/p&gt;

&lt;p&gt;Twing Data offers to redact query text at ingestion, replacing it with a constant placeholder token​. This way, we can still analyze query structures (e.g. to identify patterns or heavy queries) without storing any actual identifiers or literals. Keep in mind, though, some analyses may not be as useful if we can't identify what makes different query pattern executions different from one another. We’re always looking to strike the right balance..&lt;/p&gt;

&lt;h3&gt;
  
  
  Encryption (Data at Rest &amp;amp; In Transit)
&lt;/h3&gt;

&lt;p&gt;Nowadays, it’s safe to assume that any enterprise integration is going to use a secure protocol, but it’s still good to make sure. Any person or server that is accessing your data (metadata or otherwise) should be doing so securely. If not — run!&lt;/p&gt;

&lt;p&gt;The (meta)data itself should also be stored securely. Twing Data leverages Google BigQuery which encrypts data at rest and adds additional access controls and auditing.&lt;/p&gt;

&lt;h2&gt;
  
  
  App Architecture
&lt;/h2&gt;

&lt;p&gt;The “cloud”, as we know, is generally “someone else’s servers”. It enables distributing and scaling applications across the world while optimizing cost and speed.&lt;/p&gt;

&lt;p&gt;However, as infrastructure evolves and becomes more complex, there are more points of failure and more visibility and failsafes are needed. Let’s take a look at what that means for warehouse optimization tools.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud Infrastructure Security
&lt;/h3&gt;

&lt;p&gt;Similar to data isolation, containerization or virtual machine (VM) isolation can ease security concerns, especially for more than read-only access. A setup like this may spin up VMs per tenant, either on-premise or managed by another provider like AWS or GCP.&lt;/p&gt;

&lt;p&gt;Some providers, like fly.io (which Twing Data uses for hosting our application), are easy to scale globally while maintaining security practices. Twing Data’s analyses run on demand and on-premises, interfacing securely with our databases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Logging and Monitoring
&lt;/h3&gt;

&lt;p&gt;For any organization, it’s important to log events to detect, prevent, or alert on security issues or outages. For big data, an outage can mean inaccurate insights and lost revenue, so alerting and monitoring is crucial. Similarly, monitoring and alerting help catch bugs or security vulnerabilities sooner than later (with additional context about what happened and why), making it quicker to resolve them.&lt;/p&gt;

&lt;p&gt;For example, if an API integration suddenly starts returning a high volume of 500 errors, alerting can notify the team before customers are impacted. In a security context, logs showing a spike in failed authentication attempts could indicate a brute-force attack, allowing for a proactive response. Additionally, monitoring data pipeline performance can catch bottlenecks – like a slow ETL job – that might otherwise cause reporting delays or incomplete/inaccurate analytics.&lt;/p&gt;

&lt;p&gt;Some services (like fly.io) offer built-in logging, monitoring, and alerting (e.g. Grafana and Sentry integrations) that makes it fast, easy, and secure to set up the necessary infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Company Practices
&lt;/h2&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%2Fddrigst9ld7jde1e6vk9.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%2Fddrigst9ld7jde1e6vk9.jpg" alt="Header image: fire extinguisher hanging on a wall" width="800" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Photo by Piotr Chrobot on Unsplash&lt;/p&gt;

&lt;p&gt;Lastly, company practices can give additional insight into what to expect when working with them. Do they take security seriously as part of their culture, or just to check some boxes? If a problem occurs, how likely are they to fix it within a given timeframe?&lt;/p&gt;

&lt;h3&gt;
  
  
  Compliance &amp;amp; Certification
&lt;/h3&gt;

&lt;p&gt;Some certifications (SOC 2 Type I) evaluate security at a single point in time, and others (SOC 2 Type II) evaluate security over a period of time. These evaluations are based on many factors such as access controls, monitoring &amp;amp; logging, incident response, data encryption &amp;amp; integrity, secure development practices, and others.&lt;/p&gt;

&lt;p&gt;Seeing that a vendor has these certifications gives a clear indication of their security practices. It’s something Twing Data is working towards!&lt;/p&gt;

&lt;h3&gt;
  
  
  Incident Response &amp;amp; Recovery
&lt;/h3&gt;

&lt;p&gt;A company’s processes for detecting, responding to, and recovering from security incidents is just as important as their processes to avoid and alert on such incidents. After all, an alert is only valuable if it cuts through the noise and prompts action – otherwise, it’s just another overlooked warning.&lt;/p&gt;

&lt;p&gt;SOC2 certification requires to document related processes thoroughly and test them at least annually. Of course, companies without the certification can still have a plan in place.&lt;/p&gt;

&lt;p&gt;These processes cover responsibilities and actions that would be performed internally (e.g. patching a vulnerability) and externally (e.g. notifying affected users) with clear steps for identifying, containing, mitigating, and resolving incidents. In some cases, a service level agreement (SLA) between the vendor and customer may dictate time-to-resolution requirements, ensuring accountability.&lt;/p&gt;

&lt;p&gt;For example, if a critical data breach occurs, an engineering team might immediately isolate the affected systems while security and legal teams work together to draft and send a breach notification within the SLA-defined timeframe.&lt;/p&gt;

&lt;h3&gt;
  
  
  Secure Development Practices
&lt;/h3&gt;

&lt;p&gt;Cutting-edge companies should embed security throughout the development lifecycle. Increasingly, security and code quality checks are happening earlier in the development lifecycle. Tools like static code analysis, linting, code reviews, and automated security scanning help catch issues before they even reach a staging environment.&lt;/p&gt;

&lt;p&gt;Security-minded SaaS providers also conduct regular automated and manual penetration testing on their application and infrastructure​, and any critical findings are fixed as part of their vulnerability management process.&lt;/p&gt;

&lt;p&gt;By embedding security holistically within the development workflow, companies can better safeguard sensitive data and reduce the overall cost and risk of fixing vulnerabilities later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Third-Party Risk Management
&lt;/h3&gt;

&lt;p&gt;Third-party risks typically encompass technical topics such as security breaches and downtime, but can include more qualitative risks like reputational risk.&lt;/p&gt;

&lt;p&gt;Using reputable third parties like GCP and fly.io that have their own certifications is one way to mitigate risk that comes with outsourcing to external service providers, especially when it comes to some of the topics above around data and cloud security — items that are most commonly outsourced (since maintaining your own hardware comes with its own high costs and risks).&lt;/p&gt;

&lt;p&gt;Vendors can also be classified based on risk — a database vendor is of higher risk than an office supplier, for example, and possibly doesn’t need to be reassessed as frequently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing the right data vendor for your business
&lt;/h2&gt;

&lt;p&gt;As I was writing and researching for this article, the main takeaway became clear: it's essential to choose a vendor that analyzes your company's unique and mission-critical data effectively and securely. I hope these findings help you mitigate that risk by knowing what to look for and avoid when digging into vendors’ security policies.&lt;/p&gt;

</description>
      <category>security</category>
      <category>database</category>
      <category>architecture</category>
      <category>softwareengineering</category>
    </item>
    <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>
    <item>
      <title>Setting up Create React App with @emotion/react v11 and and TypeScript 4.5.5 (Bonus: replacing npm with yarn)</title>
      <dc:creator>Simon Goldin</dc:creator>
      <pubDate>Sun, 23 Jan 2022 23:01:52 +0000</pubDate>
      <link>https://dev.to/goldins/setting-up-create-react-app-with-emotionreact-v11-and-and-typescript-455-bonus-replacing-npm-with-yarn-3pg</link>
      <guid>https://dev.to/goldins/setting-up-create-react-app-with-emotionreact-v11-and-and-typescript-455-bonus-replacing-npm-with-yarn-3pg</guid>
      <description>&lt;h1&gt;
  
  
  Intro
&lt;/h1&gt;

&lt;p&gt;A quick, straightforward and (as of January 2022) up-to-date guide for setting up create-react-app with the latest versions of &lt;a href="https://emotion.sh" rel="noopener noreferrer"&gt;emotion&lt;/a&gt; (11.7.1) and TypeScript (4.5.5).&lt;/p&gt;

&lt;p&gt;This also documents the steps needed to migrate from &lt;code&gt;npm&lt;/code&gt; to &lt;code&gt;yarn&lt;/code&gt; v3, with offline cache, if you want.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;initialize the project.&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx create-react-app my-app --template typescript
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;install &lt;code&gt;@emotion/react&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i @emotion/react
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;emotion v11 includes &lt;code&gt;emotion-theming&lt;/code&gt; (&lt;a href="https://emotion.sh/docs/theming" rel="noopener noreferrer"&gt;https://emotion.sh/docs/theming&lt;/a&gt;)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add a property to &lt;code&gt;tsconfig.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"jsxImportSource": "@emotion/react"
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add &lt;code&gt;jsxImportSource&lt;/code&gt; pragma to &lt;em&gt;top&lt;/em&gt; (important) of each tsx file:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/** @jsxImportSource @emotion/react */
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Optionally, remove the react import.&lt;/p&gt;

&lt;h1&gt;
  
  
  Bonus: &lt;code&gt;npm&lt;/code&gt; to &lt;code&gt;yarn 3&lt;/code&gt; migration
&lt;/h1&gt;

&lt;p&gt;There aren't a lot of documentation about the latest (again, as of January 2022) version of yarn, &lt;code&gt;3.1.1&lt;/code&gt;. Here's the steps I took to move from &lt;code&gt;npm&lt;/code&gt; and enable offline cache.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Remove &lt;code&gt;package-lock.json&lt;/code&gt; and &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;add the following to &lt;code&gt;.gitignore&lt;/code&gt; (I like to include a link for context):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
.yarn/*
!.yarn/cache
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;add to &lt;code&gt;yarnrc.yml&lt;/code&gt; (create it if needed):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-3.1.1.cjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;run &lt;code&gt;yarn&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You're done! &lt;code&gt;yarn start&lt;/code&gt; should start the dev environment as expected.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optional: replace references to &lt;code&gt;npm&lt;/code&gt; in the &lt;code&gt;README.md&lt;/code&gt;. &lt;code&gt;npm run&lt;/code&gt; -&amp;gt; &lt;code&gt;yarn&lt;/code&gt;, &lt;code&gt;npm start&lt;/code&gt; -&amp;gt; &lt;code&gt;yarn start&lt;/code&gt;, &lt;code&gt;npm test&lt;/code&gt; -&amp;gt; &lt;code&gt;yarn test&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Configuring your React Starter Kit</title>
      <dc:creator>Simon Goldin</dc:creator>
      <pubDate>Sun, 07 Jun 2020 22:18:54 +0000</pubDate>
      <link>https://dev.to/goldins/configuring-your-react-starter-kit-561k</link>
      <guid>https://dev.to/goldins/configuring-your-react-starter-kit-561k</guid>
      <description>&lt;p&gt;&lt;em&gt;Previously, I outlined things to consider when picking a starter kit. This post goes into what I do after making a selection.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Now that I picked a starter kit (in this case, NextJS's &lt;a href="https://github.com/vercel/next.js/blob/v9.4.4/examples/with-typescript-eslint-jest" rel="noopener noreferrer"&gt;&lt;code&gt;with-typescript-eslint-jest&lt;/code&gt;&lt;/a&gt;), as much as I want to start actually &lt;em&gt;building&lt;/em&gt; something, I want to lay some foundation first. Of course, I'll build it first to make sure it works! In my case, &lt;code&gt;yarn &amp;amp;&amp;amp; yarn dev&lt;/code&gt; is enough to make sure it's working as expected. Other frameworks and libraries may have other commands, and &lt;code&gt;npm&lt;/code&gt; may be used in place of &lt;code&gt;yarn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Once I know it's working, I'll do a few things to modernize the project and enforce consistency and code safety in the future:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update dependencies&lt;/li&gt;
&lt;li&gt;Configure ESLint, Prettier, and TypeScript&lt;/li&gt;
&lt;li&gt;Rebuild and run tests&lt;/li&gt;
&lt;li&gt;Commit and push&lt;/li&gt;
&lt;li&gt;Set up the IDE&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Taking these steps will introduce consistency and safety which will help avoid bugs and friction in the future.&lt;/p&gt;

&lt;p&gt;Please read on for details on each of these steps!&lt;/p&gt;

&lt;h2&gt;
  
  
  Update dependencies
&lt;/h2&gt;

&lt;p&gt;Once I knew it was working, I jumped back into the &lt;code&gt;package.json&lt;/code&gt; file. This time in my IDE, rather than the repository linked above. As I noted in my previous post, a few dependencies were a bit out of date so I updated them to use the latest and greatest.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;yarn upgrade&lt;/code&gt; will update packages depending on how they are required in your &lt;code&gt;package.json&lt;/code&gt;. Typically, a caret (&lt;code&gt;^&lt;/code&gt;) is used to avoid breaking changes (i.e. major versions will not be updated) when updating dependencies (a &lt;code&gt;~&lt;/code&gt; can be used to only pick up "patch" changes, usually bug fixes). Therefore, dependencies bundled with your application may be minor or patch versions ahead of what your &lt;code&gt;package.json&lt;/code&gt; file says.&lt;/p&gt;

&lt;p&gt;Since this is a fresh project, I want to pick up major changes too, since breaking changes are unlikely to affect anything or will be easy to resolve. My preferred IDE, &lt;a href="https://www.jetbrains.com/webstorm/" rel="noopener noreferrer"&gt;WebStorm&lt;/a&gt; has a hotkey (ctrl + space) to find and include latest dependencies:&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%2Fi%2Fdlarr2w5u478l3x6qnf1.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%2Fi%2Fdlarr2w5u478l3x6qnf1.png" alt="Updating Dependencies in WebStorm" width="508" height="209"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.jetbrains.com/help/webstorm/installing-and-removing-external-software-using-node-package-manager.html#ws_npm_update_dependencies_from_package_json" rel="noopener noreferrer"&gt;WebStorm documentation&lt;/a&gt; has an explanation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code completion for previous package versions. When you press ⌃Space or start typing a version different from the latest one, WebStorm displays a suggestion list with all the previous versions of the package.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Visual Studio Code has a plugin, &lt;a href="https://marketplace.visualstudio.com/items?itemName=pflannery.vscode-versionlens" rel="noopener noreferrer"&gt;Version Lens&lt;/a&gt;, that displays the latest version and lets you update your &lt;code&gt;package.json&lt;/code&gt; with a click:&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%2Fi%2Fbsn46h4osz4ivkw18mso.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%2Fi%2Fbsn46h4osz4ivkw18mso.png" alt="Updating Dependencies in VS Code" width="327" height="75"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In my case, most packages already used the most recent major versions so it wasn't likely that these updates caused any problems. I ran &lt;code&gt;yarn dev&lt;/code&gt; to quickly make sure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configurations
&lt;/h2&gt;

&lt;p&gt;I purposefully picked a starter kit that has some extra &lt;code&gt;devDependencies&lt;/code&gt; included, such as Prettier, Jest, and ESLint, but they might not align with my preferences or needs. Before I decide on architecture or start writing new code, I want to configure these settings. Now that I have the most recent versions of my dependencies, I might also be able to leverage some new features that weren't available before.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://eslint.org/" rel="noopener noreferrer"&gt;ESLint&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;In the &lt;code&gt;.eslintrc.json&lt;/code&gt;, I replaced &lt;code&gt;0&lt;/code&gt;s with &lt;code&gt;"off"&lt;/code&gt;s and &lt;code&gt;2&lt;/code&gt;s with &lt;code&gt;"error"&lt;/code&gt;s because I like the explicitness. The &lt;code&gt;"extends"&lt;/code&gt; array, which uses preset recommendations, sets a good baseline, and customizations are up to personal preference. I make a few customizations to make linting a little stricter, like setting &lt;code&gt;@typescript-eslint/no-explicit-any&lt;/code&gt; to &lt;code&gt;"warn"&lt;/code&gt;. Other rules I leave off because TypeScript and Prettier remove the need for them (such as &lt;code&gt;react/prop-types&lt;/code&gt; and &lt;code&gt;@typescript-eslint/indent&lt;/code&gt;, respectively).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;"extends"&lt;/code&gt; list has a note about Prettier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Uncomment the following lines to enable eslint-config-prettier
// Is not enabled right now to avoid issues with the Next.js repo
"prettier",
"prettier/@typescript-eslint"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is straightforward enough, so I do that, and then move on to the &lt;code&gt;.prettierrc&lt;/code&gt; configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Prettier is an opinionated code formatter &lt;a href="https://prettier.io/docs/en/option-philosophy.html" rel="noopener noreferrer"&gt;to stop all the on-going debates over styles.&lt;/a&gt;. The &lt;a href="https://prettier.io/docs/en/configuration.html" rel="noopener noreferrer"&gt;&lt;code&gt;.prettierrc&lt;/code&gt;&lt;/a&gt; file allows you to define a few options such as spaces or tabs (a hottly debated topic!) and whether or not to omit semicolons, or trailing commas.&lt;/p&gt;

&lt;p&gt;I'm not going to go into details about what rules I set; the specific rules aren't as important as having the code style consistency that Prettier provides.&lt;/p&gt;

&lt;p&gt;I ran this starter kit's &lt;code&gt;format&lt;/code&gt; script to reformat my files with my new settings.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.typescriptlang.org/" rel="noopener noreferrer"&gt;TypeScript&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;TypeScript, an extension of JavaScript that adds type safety, has an optional configuration file, &lt;code&gt;tsconfig.json&lt;/code&gt;. The file included in &lt;code&gt;with-typescript-eslint-jest&lt;/code&gt; has some preset options. I set &lt;code&gt;"strict"&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt; to turn on "strict mode" which will add some additional safety to my codebase. &lt;code&gt;strict&lt;/code&gt; is a combination of other TypeScript options: &lt;code&gt;noImplicitAny&lt;/code&gt;, &lt;code&gt;noImplicitThis&lt;/code&gt;, &lt;code&gt;alwaysStrict&lt;/code&gt;, &lt;code&gt;strictBindCallApply&lt;/code&gt;, &lt;code&gt;strictNullChecks&lt;/code&gt;, &lt;code&gt;strictFunctionTypes&lt;/code&gt; and &lt;code&gt;strictPropertyInitialization&lt;/code&gt;. Setting the &lt;code&gt;strict&lt;/code&gt; option will tell TypeScript to be more strict around &lt;code&gt;null&lt;/code&gt; and &lt;code&gt;undefined&lt;/code&gt; (&lt;code&gt;strictNullChecks&lt;/code&gt;) and will prevent code from remaining un-typed (&lt;code&gt;noImplicitAny&lt;/code&gt;), among other things.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Configurations
&lt;/h3&gt;

&lt;p&gt;There are other configurations that you might want to create or update, such as &lt;code&gt;jest.config.js&lt;/code&gt; (for unit tests) or &lt;code&gt;next.config.js&lt;/code&gt; (for Next.JS), but since those are related to your specific application, that's not necessary this early on. Another benefit of starting with a starter kit is that they will be set up properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuild and Run Tests
&lt;/h2&gt;

&lt;p&gt;Once my dependencies are up to date and configurations are done, I ran &lt;code&gt;yarn dev&lt;/code&gt; to make sure there were not regressions. I also ran &lt;code&gt;yarn test&lt;/code&gt; for good measure, and discovered that a test failed because a snapshot was not updated, so I had a great opportunity to fix it by opening a &lt;a href="https://github.com/vercel/next.js/pull/13847/files" rel="noopener noreferrer"&gt;pull request&lt;/a&gt;!&lt;/p&gt;

&lt;h2&gt;
  
  
  Commit and push
&lt;/h2&gt;

&lt;p&gt;If using &lt;code&gt;git&lt;/code&gt;, running the &lt;code&gt;commit&lt;/code&gt; and &lt;code&gt;push&lt;/code&gt; commands at this point will bring up potential issues with version control such as permissions and making sure the repository actually exists and some quality checks may also run, facilitated by &lt;a href="https://git-scm.com/docs/githooks" rel="noopener noreferrer"&gt;git hooks&lt;/a&gt;). Running these &lt;code&gt;git&lt;/code&gt; commands will also show whether or not they are set up and working properly.&lt;/p&gt;

&lt;p&gt;Auto-formatting and linting scripts are often set up as a pre-commit scripts. Both of these actions are usually quick since they can run on individual files and are not concerned with the rest of the code.&lt;/p&gt;

&lt;p&gt;TypeScript compilation typically takes longer, and depending on your workflow can be frustrating to run as a "pre-commit" check. As a "pre-push" check, it will make sure that other people working on the same code aren't picking up code that doesn't compile!&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pre-commit&lt;/code&gt; and &lt;code&gt;pre-push&lt;/code&gt; quality gates (as well as &lt;a href="https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks" rel="noopener noreferrer"&gt;others&lt;/a&gt;) can be customized to your, or the team's, needs.&lt;/p&gt;

&lt;p&gt;In my case, &lt;code&gt;with-typescript-eslint-jest&lt;/code&gt; was already set up with Prettier and ESLint to format and lint changed files as a &lt;code&gt;pre-commit&lt;/code&gt; hook, and TypeScript compilation was included as a &lt;code&gt;pre-push&lt;/code&gt; hook.&lt;/p&gt;

&lt;h2&gt;
  
  
  IDE Setup
&lt;/h2&gt;

&lt;p&gt;Now that my application has the configurations I want and I know they work, I'll set up my IDE to hook into them. With WebStorm, a lot of these settings are done in the Preferences panel. Searching for Prettier, for example, allows you to automatically pick up the &lt;code&gt;.prettierrc&lt;/code&gt; settings (I also like to enable "Run on save"). There are similar options for ESLint and TypeScript, which may be configured automatically.&lt;/p&gt;

&lt;p&gt;Visual Studio Code's extensive extension collection covers these features as well: &lt;a href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode" rel="noopener noreferrer"&gt;Prettier - Code Formatter&lt;/a&gt; and &lt;a href="https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint" rel="noopener noreferrer"&gt;ESLint&lt;/a&gt; plugins can be installed and configured as needed and have their own extensive documentation and examples.&lt;/p&gt;

&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;p&gt;I haven't really built anything quite yet but I know that when I do, future errors will be caught sooner than later since now I have a few places where errors are surfaced: my IDE, before code is committed, and before code is pushed (and likely shared). These automated tools won't find &lt;em&gt;all&lt;/em&gt; errors, but the ones they do find are the most common, easiest to fix, and can be underlying errors that cause real bugs.&lt;/p&gt;

&lt;p&gt;Setting up Prettier will help me focus on what I'm building. On a team, introducing automated formatting in the workflow will reduce the time code changes spend in code reviews, and will keep the code consistent.&lt;/p&gt;

&lt;p&gt;Doing this now, before I've actually written any code, will save me headaches in the future.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>frontend</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Choosing a React Starter Kit</title>
      <dc:creator>Simon Goldin</dc:creator>
      <pubDate>Thu, 04 Jun 2020 01:04:00 +0000</pubDate>
      <link>https://dev.to/goldins/choosing-a-react-starter-kit-1dc9</link>
      <guid>https://dev.to/goldins/choosing-a-react-starter-kit-1dc9</guid>
      <description>&lt;p&gt;&lt;em&gt;General guide for choosing a starter kit (aka starter library, aka boilerplate) that's right for you and your team.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;This post will explore choosing a starter kit, using my experience building &lt;del&gt;this&lt;/del&gt; a blog (note: the blog is not complete yet; I am posting this here in the meantime!) as an example. This will be the first post in a series that comes out of working on this website.&lt;/p&gt;

&lt;p&gt;Since this is my first post, I want to start by sharing my experience. Coming into this, I've worked with React and TypeScript for about 5 years and I've been working in web development professionally for the past 10 years. My career has spanned finance, ed-tech, ad-tech, and currently internet security, as well as some freelancing.&lt;/p&gt;

&lt;p&gt;I've typically been a full-stack engineer, usually focusing on front-end and user experience, which is where I hope I can provide the most help.&lt;/p&gt;

&lt;p&gt;With that, let's (yarn?) start!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why choose a starter kit at all?
&lt;/h2&gt;

&lt;p&gt;There's value in building an application from scratch, particularly for gaining valuable learning experience as you go, and to be able to specify exactly what you want, and nothing else.&lt;/p&gt;

&lt;p&gt;You can also choose to use a starter kit: a minimal application with pre-defined dependencies and some dummy content already in place.&lt;/p&gt;

&lt;p&gt;A starter kit does a lot of heavy lifting for you out of the box and is usually a way to hit the ground running. When my team first picked up React, we went with a starter kit (&lt;a href="https://github.com/facebook/create-react-app" rel="noopener noreferrer"&gt;Create React App&lt;/a&gt;) which abstracted a lot of the complexity away from us, and we were able to focus on building the application and not worry (as much) about tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose a starter kit
&lt;/h2&gt;

&lt;p&gt;I've gone through this process a handful of times, so at this point I think it may be helpful to others (and future me) to document what I look for and avoid. In a future post, I will also go into some detail about what I do after making a decision. These factors are mainly about the dependencies that are included, since you're going to change the content of the application anyways.&lt;/p&gt;

&lt;p&gt;Luckily, it's not too hard to try a few different options before settling on one, and future you (or your team) will thank present you for making a good decision.&lt;/p&gt;

&lt;h3&gt;
  
  
  What to look for
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Minimal&lt;/li&gt;
&lt;li&gt;Good Tooling&lt;/li&gt;
&lt;li&gt;Officially Supported&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Minimal
&lt;/h3&gt;

&lt;p&gt;When looking for a starter kit in the past, I came across &lt;em&gt;a lot&lt;/em&gt; of options. After digging into them a bit, there was a lot of things I either didn't need, or didn't know if I needed. Having unnecessary dependencies can bias you into using them and may not align with you or your team's preference. By the time, and if, a need for such a dependency arises, there might be (in the JavaScript ecosystem's case, likely will be) something better.&lt;/p&gt;

&lt;p&gt;You'll also probably notice some outdated packages that you'll want to update sooner than later, and having fewer packages to update will make it easier to update them.&lt;/p&gt;

&lt;p&gt;For example, updating a library such as Redux (and its peer dependencies, and their TypeScript definitions) might introduce breaking changes into the boilerplate content which you'll be re-writing anyways. By the time you want to introduce state management, there will likely be a newer version, or a more suitable alternative.&lt;/p&gt;

&lt;h4&gt;
  
  
  Good Tooling
&lt;/h4&gt;

&lt;p&gt;By "tooling", I am essentially referring to specific dev dependencies. You'll likely want things like formatting, linting, and testing systems in place eventually, so you may as well get a starter kit that already has them integrated for you. Each of these will have its own options to choose from (like formatting rules), so picking a starting kit that has these out-of-the-box will mean they will work with each other (for example: &lt;a href="https://eslint.org/" rel="noopener noreferrer"&gt;ESLint&lt;/a&gt;, &lt;a href="https://prettier.io/" rel="noopener noreferrer"&gt;Prettier&lt;/a&gt;, and TypeScript configurations working seamlessly together).&lt;/p&gt;

&lt;h4&gt;
  
  
  Officially Supported
&lt;/h4&gt;

&lt;p&gt;Official starter kits, like those found on a framework's or library's website, are going to be more general-purpose, and not something that was created for a specific use case in the past and no longer maintained. They'll also be more up-to-date, and in some cases have a built-in mechanism to update itself. Official kits typically have the same licenses as their library or framework which may be important if a license is something you need to consider.&lt;/p&gt;

&lt;p&gt;Next.JS has a set of &lt;a href="https://github.com/vercel/next.js/tree/master/examples" rel="noopener noreferrer"&gt;many examples&lt;/a&gt; that, while not the most user-friendly, is easy to navigate and covers most needs. These examples are also used to populate the options when you run the &lt;code&gt;create next-app&lt;/code&gt; script, so once you pick one, installing it is a snap.&lt;/p&gt;

&lt;p&gt;Gatsby has &lt;a href="https://www.gatsbyjs.org/starters/?v=2" rel="noopener noreferrer"&gt;hundreds of starter kits&lt;/a&gt; available with easy like previews. However, only three of them are maintained by the Gatsby team themselves. The list can be filtered by Gatsby Version, dependencies, and categories ("Official", "Blog").&lt;/p&gt;

&lt;p&gt;While the only official starter kit from the React team, &lt;a href="https://github.com/facebook/create-react-app" rel="noopener noreferrer"&gt;Create React App&lt;/a&gt; is likely the most mature stater kit in the React ecosystem, easy to keep up to date, and sufficiently configurable while abstracting a lot of the complexity away.&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;With these factors in mind, I started looking through the Next.JS examples repo. I knew I wanted to use TypeScript, so that narrowed my choices down to a manageable handful.&lt;/p&gt;

&lt;p&gt;With these points in mind, I picked &lt;a href="https://github.com/vercel/next.js/tree/canary/examples/with-typescript-eslint-jest" rel="noopener noreferrer"&gt;with-typescript-eslint-jest&lt;/a&gt; for Next.JS. Taking away aspects that I don't need (or at least don't think I need yet), slimmed the list down to four:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/vercel/next.js/blob/v9.4.4/examples/blog-starter-typescript/package.json" rel="noopener noreferrer"&gt;&lt;code&gt;blog-starter-typescript&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vercel/next.js/blob/v9.4.4/examples/with-typescript/package.json" rel="noopener noreferrer"&gt;&lt;code&gt;with-typescript&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vercel/next.js/blob/v9.4.4/examples/with-typescript-styled-components/package.json" rel="noopener noreferrer"&gt;&lt;code&gt;with-typescript-styled-components&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/vercel/next.js/blob/v9.4.4/examples/with-typescript-eslint-jest/package.json" rel="noopener noreferrer"&gt;&lt;code&gt;with-typescript-eslint-jest&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Looking into each &lt;code&gt;package.json&lt;/code&gt; file (linked above), I knew that TypeScript will add some extra dependencies for type definitions, but overall they were all manageable.&lt;/p&gt;

&lt;p&gt;I knew I wanted a CSS-in-JS library, but wasn't sure if I wanted to use &lt;a href="https://styled-components.com/" rel="noopener noreferrer"&gt;&lt;code&gt;styled-components&lt;/code&gt;&lt;/a&gt;, so I eliminated &lt;code&gt;with-typescript-styled-components&lt;/code&gt; from my list first.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;blog-starter-typescript&lt;/code&gt; had some useful blog- and markdown-specific libraries (&lt;code&gt;remark&lt;/code&gt;, &lt;code&gt;gray-matter&lt;/code&gt;), though I wasn't sure if I would be using them. It was also &lt;em&gt;almost&lt;/em&gt; on the latest version of TypeScript (although it's in the &lt;code&gt;dependencies&lt;/code&gt; section rather than &lt;code&gt;devDependencies&lt;/code&gt;). It also included some other libraries I knew I would be removing (&lt;code&gt;tailwindcss&lt;/code&gt;). Writing this post and looking into this example more, I found some other questionable decisions such as the inclusion of &lt;code&gt;@types/jest&lt;/code&gt; but not &lt;code&gt;jest&lt;/code&gt; itself and the inclusion of &lt;code&gt;remark-html&lt;/code&gt; which has the following disclaimer on &lt;a href="https://github.com/remarkjs/remark-html" rel="noopener noreferrer"&gt;its README&lt;/a&gt; "it’s probably smarter to use &lt;code&gt;remark-rehype&lt;/code&gt; directly". These are minor points, but all reduce my confidence in picking it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;with-typescript-eslint-jest&lt;/code&gt; had by far the largest list of dependencies, but it was everything I would have included anyways, and nothing I wouldn't have. The nature of &lt;code&gt;jest&lt;/code&gt; and &lt;code&gt;eslint&lt;/code&gt; typically require additional &lt;code&gt;@types&lt;/code&gt; in the &lt;code&gt;devDependencies&lt;/code&gt; section, but this example had the same list of &lt;code&gt;dependencies&lt;/code&gt; as &lt;code&gt;with-typescript&lt;/code&gt; itself. Aside from the given ESLint and Jest, the example came with Prettier, lint-staged and husky to automatically format the files when you commit your changes. As a bonus, &lt;code&gt;jest-watch-typeahead&lt;/code&gt;, which lets you filter your tests as you're running them, is probably something I would not have installed but eventually wished I had. All dependencies were relatively up to date, and having ESLint and Jest included would save me some installation and configuration steps I would have to do with the bare TypeScript example, so I settled on &lt;code&gt;with-typescript-eslint-jest&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;While a complete checklist is not possible, I hope this provides some guidance for your next application. Whether it's a side project to get your career off the ground, an internal tool or your team's next project, carefully considering and vetting your options pays off in the future!&lt;/p&gt;

&lt;p&gt;My &lt;a href="https://dev.to/goldins/configuring-your-react-starter-kit-561k"&gt;next post&lt;/a&gt; will go into what I did next: from updating configurations to laying down foundation for easier maintenance in the future.&lt;/p&gt;

</description>
      <category>react</category>
      <category>typescript</category>
      <category>frontend</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
