<?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: Jason Lengstorf</title>
    <description>The latest articles on DEV Community by Jason Lengstorf (@jlengstorf).</description>
    <link>https://dev.to/jlengstorf</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%2F88635%2Ff553bcb1-da02-49ed-99b9-9d7f8e0121e0.jpg</url>
      <title>DEV Community: Jason Lengstorf</title>
      <link>https://dev.to/jlengstorf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jlengstorf"/>
    <language>en</language>
    <item>
      <title>🌶️ Newest Episode of Leet Heat: A Game Show For Developers!</title>
      <dc:creator>Jason Lengstorf</dc:creator>
      <pubDate>Tue, 25 Mar 2025 13:00:00 +0000</pubDate>
      <link>https://dev.to/jlengstorf/newest-episode-of-leet-heat-a-game-show-for-developers-gim</link>
      <guid>https://dev.to/jlengstorf/newest-episode-of-leet-heat-a-game-show-for-developers-gim</guid>
      <description>&lt;p&gt;Today we're excited to announce the newest episode of &lt;em&gt;Leet Heat&lt;/em&gt;: A game show for web developers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens when two very experienced developers go head-to-head on web dev trivia — with fiery consequences for wrong answers?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this episode, principal engineer Chris takes on staff engineer Jeremy to see who can bring the knowledge — or face the heat! 🌶️🔥&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://codetv.link/lh" rel="noopener noreferrer"&gt;&lt;em&gt;Leet Heat&lt;/em&gt;&lt;/a&gt; is a game show created by web developers, for web developers. Host Mark Techson and executive producer Jason Lengstorf cook up challenging questions and even more challenging bites in this original series from &lt;a href="https://codetv.dev" rel="noopener noreferrer"&gt;CodeTV&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;em&gt;Leet Heat&lt;/em&gt; works
&lt;/h2&gt;

&lt;p&gt;In every episode, two developers face off to test their web dev knowledge. They spin the wheel to choose a category, then answer a series of questions. Correct answers earn points. Wrong answers lose points — and require the dev to eat a Deno nuggie drenched in hot sauce that gets hotter with every wrong answer.&lt;/p&gt;

&lt;p&gt;Test your knowledge alongside the contestants: how many spicy bites would you have to eat?&lt;/p&gt;

&lt;h2&gt;
  
  
  Categories in this episode
&lt;/h2&gt;

&lt;p&gt;Every episode tests the devs across the full stack of web dev knowledge. In this episode, they'll need to demonstrate their knowledge of:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shipping Fast&lt;/li&gt;
&lt;li&gt;Authentication&lt;/li&gt;
&lt;li&gt;CSS&lt;/li&gt;
&lt;li&gt;Acronyms&lt;/li&gt;
&lt;li&gt;Accessibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Thank you to the sponsors that make &lt;em&gt;Leet Heat&lt;/em&gt; possible
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://codetv.link/neon" rel="noopener noreferrer"&gt;Neon&lt;/a&gt; provides a serverless Postgres platform with database branching, ephemeral environments, and a lot more&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://codetv.link/clerk" rel="noopener noreferrer"&gt;Clerk&lt;/a&gt; is a comprehensive user management platform with drop-in components to get auth handled in your app&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://deno.com/nuggies" rel="noopener noreferrer"&gt;Deno&lt;/a&gt;‬ is a batteries-included platform for TypeScript and JavaScript projects&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://codetv.link/codepen" rel="noopener noreferrer"&gt;CodePen&lt;/a&gt; lets you build, share, and remix frontend ideas with a community of talented and creative developers&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://dtsx.io/lwj" rel="noopener noreferrer"&gt;DataStax&lt;/a&gt;‬ is the fastest way to build AI applications&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Use AI to moderate abusive and vulgar comments (full tutorial)</title>
      <dc:creator>Jason Lengstorf</dc:creator>
      <pubDate>Mon, 24 Jul 2023 00:00:00 +0000</pubDate>
      <link>https://dev.to/jlengstorf/use-ai-to-moderate-abusive-and-vulgar-comments-full-tutorial-2h7j</link>
      <guid>https://dev.to/jlengstorf/use-ai-to-moderate-abusive-and-vulgar-comments-full-tutorial-2h7j</guid>
      <description>&lt;p&gt;Build an internal dashboard to view and moderate comments in this full tutorial. Plus, learn how to use OpenAI to automatically flag the worst comments.&lt;/p&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/cdgj96bFXhs"&gt;
&lt;/iframe&gt;
&lt;br&gt;
&lt;a href="https://github.com/learnwithjason/airplane-content-moderation" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;Want to jump to the end? Check out the source code on GitHub.&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;Internal tools don't get prioritized. That's why so many developers have horror stories of teams running superadmin commands against the production database that they copy-pasted from a doc somewhere.&lt;/p&gt;

&lt;p&gt;In this tutorial, you'll &lt;strong&gt;learn how to build an internal dashboard to moderate a custom comments database.&lt;/strong&gt; You won't have to write much code, but everything you create will be stored as human-readable code that can be checked into source control and edited manually without breaking the low-code workflow.&lt;/p&gt;

&lt;p&gt;As an added bonus, we'll also look at how we can &lt;strong&gt;integrate AI (using OpenAI/Chat-GPT integrated into Airplane) to automatically flag the most abusive and vulgar comments&lt;/strong&gt;, decreasing the psychic damage taken by human moderators.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Thanks to &lt;a href="https://lwj.dev/airplane" rel="noopener noreferrer"&gt;Airplane&lt;/a&gt; for sponsoring this tutorial. Go get a free account so you can follow along with this tutorial and start using their developer platform for building internal tools quickly and with minimal code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Create a new Airplane project
&lt;/h2&gt;

&lt;p&gt;To start, &lt;a href="https://lwj.dev/airplane" rel="noopener noreferrer"&gt;register for or sign into your Airplane account&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, &lt;a href="https://docs.airplane.dev/platform/airplane-cli?utm_source=learnwithjason&amp;amp;utm_medium=tutorial&amp;amp;utm_campaign=content-moderation-dashboard" rel="noopener noreferrer"&gt;install the Airplane CLI&lt;/a&gt; so you can develop locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;airplanedev/tap/airplane
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clone the starter repo, then move into it and start Airplane&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# clone the start branch of the repo&lt;/span&gt;
gh repo clone learnwithjason/airplane-content-moderation &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;-b&lt;/span&gt; start

&lt;span class="c"&gt;# move into the cloned app directory&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;airplane-content-moderation/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside, this app contains the boilerplate for a project — such as a &lt;code&gt;tsconfig.json&lt;/code&gt; and a &lt;code&gt;package.json&lt;/code&gt; — as well as a file called &lt;code&gt;airplane.yaml&lt;/code&gt;. The only thing inside this file is a note about the Node version (set to Node 18). This is a pretty cool feature of Airplane: there's very little boilerplate required.&lt;/p&gt;

&lt;p&gt;There's also an example app folder, which you can ignore for now. We'll come back to that after we've got the Airplane tasks and views built.&lt;/p&gt;

&lt;p&gt;Inside the project folder, use the Airplane CLI to start the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# start Airplane in dev mode (will prompt for login on first run)&lt;/span&gt;
airplane dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time you run this command, you'll be asked to log in.&lt;/p&gt;

&lt;p&gt;Next, the CLI will start the dev server and give you the option to press enter to open the Airplane Studio, which is a UI for local development that updates your local files in real time. Press enter to open the studio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Set up the database and initial queries
&lt;/h2&gt;

&lt;p&gt;Our first step will be to make sure we have data to work with and that we're able to read it out of our database in Airplane.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Airplane provides a demo database we can use for this tutorial. In a real project, you'll need to &lt;a href="https://docs.airplane.dev/resources/overview" rel="noopener noreferrer"&gt;connect your own database as a resource&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create a task to initialize the comments table
&lt;/h3&gt;

&lt;p&gt;To begin, let's create a table to store comments and add a few entries so we can verify things are working as expected.&lt;/p&gt;

&lt;p&gt;To get the work done that our tutorial requires, we'll be using &lt;a href="https://docs.airplane.dev/tasks/overview" rel="noopener noreferrer"&gt;Airplane tasks&lt;/a&gt;. These can take a few forms, but we'll &lt;a href="https://docs.airplane.dev/getting-started/sql" rel="noopener noreferrer"&gt;start with a SQL task&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create a new task in the Studio called "comments_db_reset" and choose the SQL option.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; You can call your tasks whatever you want. I've chosen the format of placing the affected data (comments) first, followed by what the task does.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you create the task, two new files will be created in your working directory:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;comments_db_reset.sql&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;comments_db_reset.task.yaml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's possible to edit these files directly — we'll do that for a future task — but in many cases it's much more convenient to use the Studio UI.&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%2Fz7zud9k03nk6qfvalrnl.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%2Fz7zud9k03nk6qfvalrnl.jpg" alt="the Airplane dashboard showing config for the reset comments database task" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the Studio, update the details in the "Define" section:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: Reset comments database&lt;/li&gt;
&lt;li&gt;Description: Deletes the current comments table, including all comment entries, then creates a new comments table with a few seed entries.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The local files will update as you type.&lt;/p&gt;

&lt;p&gt;Next, scroll down to the "Build" section and choose "[Demo DB]" from the "Database Resource" dropdown.&lt;/p&gt;

&lt;p&gt;Under "Query", add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;comments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt;
  &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;id&lt;/span&gt;      &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;comment&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="nb"&gt;BOOLEAN&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt;
  &lt;span class="n"&gt;comments&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flagged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'this looks so delicious omg'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'I think you suck'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'I do not want to eat this'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'eat poop'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a set of SQL instructions that removes the comments table, creates a new one with the necessary fields, and then inserts example entries into it that we can use to build out the rest of our dashboard.&lt;/p&gt;

&lt;p&gt;Our changes save as we type, so once everything is entered, we can click the "Execute Task" button in the top panel.&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%2Fvro79d72x35ojak2c5m7.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%2Fvro79d72x35ojak2c5m7.jpg" alt="the Airplane dashboard showing the result of the reset comments database task execution" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the whole process for setting up Airplane and modifying a database. I love this flow because it feels magical, but nothing that happens is "magic" or hidden from me — everything I entered in the UI is stored in my code base now.&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%2Fxq5wb1xgewt7128edxd1.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%2Fxq5wb1xgewt7128edxd1.jpg" alt="the generated YAML and SQL that makes up the reset comments database task shown in VS Code" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The UI is a convenience, not a requirement — I can choose not to use it if I prefer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a task to list all comments
&lt;/h3&gt;

&lt;p&gt;Next, let's add another task to list all of our comments. Create a new task in the Studio, choose SQL, and name it "comments_list_all". Add the following details in the "Define" section:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: List all comments&lt;/li&gt;
&lt;li&gt;Description: Lists all comments in the database, regardless of &lt;code&gt;flagged&lt;/code&gt; status&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose the demo DB from the database dropdown and add the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;comments&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt;
  &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click the "Execute Task" button and you'll see the seed comments listed on the screen.&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%2Fis0jxbyjtypxs8d5q3yb.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%2Fis0jxbyjtypxs8d5q3yb.jpg" alt="the airplane dashboard showing results of the list all comments task in a table" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a task to list flagged comments
&lt;/h3&gt;

&lt;p&gt;Next, repeat this process to create a task called "comments_list_flagged" to list only flagged comments using the following details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: List flagged comments&lt;/li&gt;
&lt;li&gt;Description: List all comments that have been flagged as abusive or otherwise problematic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choose the demo DB and add the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;comments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a task to list approved comments
&lt;/h3&gt;

&lt;p&gt;We'll also need to be able to select all the unflagged (approved) comments. For these, we can copy-paste the &lt;code&gt;comments_list_flagged&lt;/code&gt; files and make small adjustments right in the code.&lt;/p&gt;

&lt;p&gt;Rename both files to &lt;code&gt;comments_list_approved&lt;/code&gt;, keeping their respective extensions. In &lt;code&gt;comments_list_approved.task.yaml&lt;/code&gt;, make the following edits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;    &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;comments_list_approved&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;List approved comments&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;List all comments that have been approved.&lt;/span&gt;
    &lt;span class="na"&gt;sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo_db&lt;/span&gt;
      &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;comments_list_approved.sql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, replace the contents of &lt;code&gt;comments_list_approved.sql&lt;/code&gt; with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;flagged&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;comments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Build a comment moderation dashboard view
&lt;/h2&gt;

&lt;p&gt;So far, we've only looked at tasks in Airplane. Next, let's dig into how &lt;a href="https://docs.airplane.dev/views/overview" rel="noopener noreferrer"&gt;Airplane views&lt;/a&gt; work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create an Airplane view
&lt;/h3&gt;

&lt;p&gt;In the Studio, create a new view by clicking the &lt;code&gt;+&lt;/code&gt; at the top of the explorer. Give it the following details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: Comment Moderation Dashboard&lt;/li&gt;
&lt;li&gt;Description: Allows admins to see all approved comments, and optionally see flagged comments. They're also able to change the approved/flagged state of a comment and delete comments permanently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will create a new file in your local directory called &lt;code&gt;CommentModerationDashboard.airplane.tsx&lt;/code&gt; — you can rename this if you want, but we'll leave it as-is for this project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update the view with a table to display approved comments
&lt;/h3&gt;

&lt;p&gt;Inside, you'll see an example component. Replace the file contents with the following:&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;Heading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Table&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@airplane/views&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="nx"&gt;airplane&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;airplane&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;CommentModerationDashboard&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="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="nc"&gt;Stack&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;Heading&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Comment Moderation Dashboard&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Heading&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;Table&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Approved Comments"&lt;/span&gt;
        &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"comments_list_approved"&lt;/span&gt;
        &lt;span class="na"&gt;defaultPageSize&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="na"&gt;hiddenColumns&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&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;flagged&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&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;Stack&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;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;airplane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;comment_moderation_dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Comment Moderation Dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Allows admins to see all approved comments, and optionally see flagged comments. They're also able to change the approved/flagged state of a comment and delete comments permanently.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nx"&gt;CommentModerationDashboard&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://docs.airplane.dev/views/components" rel="noopener noreferrer"&gt;Airplane provides a suite of React UI components&lt;/a&gt; to make building dashboards as straightforward as snapping together components.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Stack&lt;/code&gt; is a container for content, and inside we've added a &lt;code&gt;Heading&lt;/code&gt; to let the viewer know what this dashboard is for, followed by a &lt;code&gt;Table&lt;/code&gt; to display approved comments.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;Table&lt;/code&gt; accepts a few props. The &lt;code&gt;title&lt;/code&gt; is displayed at the top, the &lt;code&gt;defaultPageSize&lt;/code&gt; tells the table how many rows to show before paginating, and &lt;code&gt;hiddenColumns&lt;/code&gt; lets us leave out columns from the table that we don't need.&lt;/p&gt;

&lt;p&gt;The really interesting prop here is the &lt;code&gt;task&lt;/code&gt; prop. &lt;a href="https://docs.airplane.dev/views/task-backed-components" rel="noopener noreferrer"&gt;Many Airplane components can be task-backed&lt;/a&gt;, which means we can perform tasks (such as loading data or performing a query) using their slugs. This is a great productivity boost, because we don't have to mess with calling APIs to load data, then looping through them to build table views — we just say, "Give me a table with the result of the task called &lt;code&gt;comments_list_approved&lt;/code&gt;" and Airplane does the rest. Neat!&lt;/p&gt;

&lt;p&gt;Once we've saved this component, we'll see a new icon pop up in the explorer for a view called "Comment Moderation Dashboard". Click on it and you'll see the layout you just built, including the approved comment entries displayed in the table.&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%2Fkciund0plka2k4zhx055.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%2Fkciund0plka2k4zhx055.jpg" alt="the comment moderation dashboard in Airplane Studio" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add another table to display flagged comments
&lt;/h3&gt;

&lt;p&gt;We also need a way to see flagged comments, so add another &lt;code&gt;Table&lt;/code&gt; that uses the &lt;code&gt;comments_list_flagged&lt;/code&gt; task:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;    import { Heading, Stack, Table } from '@airplane/views';
    import airplane from 'airplane';
&lt;span class="err"&gt;
&lt;/span&gt;    const CommentModerationDashboard = () =&amp;gt; {
        return (
            &amp;lt;Stack&amp;gt;
                &amp;lt;Heading&amp;gt;Comment Moderation Dashboard&amp;lt;/Heading&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;                &amp;lt;Table
                    title="Approved Comments"
                    task="comments_list_approved"
                    defaultPageSize={20}
                    hiddenColumns={['flagged']}
                /&amp;gt;
&lt;span class="gi"&gt;+
+               &amp;lt;Table
+                   title="Flagged Comments"
+                   task="comments_list_flagged"
+                   defaultPageSize={20}
+                   hiddenColumns={['flagged']}
+               /&amp;gt;
&lt;/span&gt;            &amp;lt;/Stack&amp;gt;
        );
    };
&lt;span class="err"&gt;
&lt;/span&gt;    export default airplane.view(
        {
            slug: 'comment_moderation_dashboard',
            name: 'Comment Moderation Dashboard',
            description:
                "Allows admins to see all approved comments, and optionally see flagged comments. They're also able to change the approved/flagged state of a comment and delete comments permanently.",
        },
        CommentModerationDashboard,
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save and the flagged comments appear, but this isn't ideal — we don't want to subject our admins to potentially abusive comments every time they load the dashboard. Instead, let's hide the flagged comments by default and only show them if the admin clicks a checkbox to confirm that they want to review flagged comments.&lt;/p&gt;

&lt;p&gt;Inside the view, let's add a &lt;code&gt;Checkbox&lt;/code&gt; from the Airplane component library, as well as the &lt;code&gt;useComponentState&lt;/code&gt; hook that will let us check whether it's checked or not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;    import {
&lt;span class="gi"&gt;+       type CheckboxState,
&lt;/span&gt;        Heading,
        Stack,
        Table,
&lt;span class="gi"&gt;+       Checkbox,
+       useComponentState,
&lt;/span&gt;    } from '@airplane/views';
    import airplane from 'airplane';
&lt;span class="err"&gt;
&lt;/span&gt;    const CommentModerationDashboard = () =&amp;gt; {
&lt;span class="gi"&gt;+       const { id, checked } = useComponentState&amp;lt;CheckboxState&amp;gt;();
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;        return (
            &amp;lt;Stack&amp;gt;
                &amp;lt;Heading&amp;gt;Comment Moderation Dashboard&amp;lt;/Heading&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;                &amp;lt;Table
                    title="Approved Comments"
                    task="comments_list_approved"
                    defaultPageSize={20}
                    hiddenColumns={['flagged']}
                /&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gi"&gt;+               &amp;lt;Checkbox
+                   id={id}
+                   label="Show flagged comments (view at your own risk!)"
+               /&amp;gt;
+
+               {checked ? (
&lt;/span&gt;                    &amp;lt;Table
                        title="Flagged Comments"
                        task="comments_list_flagged"
                        defaultPageSize={20}
                        hiddenColumns={['flagged']}
                    /&amp;gt;
&lt;span class="gi"&gt;+               ) : null}
&lt;/span&gt;            &amp;lt;/Stack&amp;gt;
        );
    };
&lt;span class="err"&gt;
&lt;/span&gt;    export default airplane.view(
        {
            slug: 'comment_moderation_dashboard',
            name: 'Comment Moderation Dashboard',
            description:
                "Allows admins to see all approved comments, and optionally see flagged comments. They're also able to change the approved/flagged state of a comment and delete comments permanently.",
        },
        CommentModerationDashboard,
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the dashboard hides the comments that could ruin someone's day by default, and they only have to be viewed if it becomes necessary to review them.&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%2F4vd1vkb2qld5tqm2y0yo.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%2F4vd1vkb2qld5tqm2y0yo.jpg" alt="the comment moderation dashboard with the checkbox unchecked and flagged comments hidden" width="800" height="490"&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%2Ffuxfznh769toe5mn3nb4.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%2Ffuxfznh769toe5mn3nb4.jpg" alt="the comment moderation dashboard with the checkbox checked and flagged comments visible" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a task to flag comments as abusive
&lt;/h3&gt;

&lt;p&gt;If a comment is approved by mistake, we need the ability to manually flag it. To do that, create a new task called "comment_flag" with the following details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: Flag comment as abusive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Under parameters, click the "Add parameter" button and add the following values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: id&lt;/li&gt;
&lt;li&gt;Description: The ID of the comment to flag.&lt;/li&gt;
&lt;li&gt;Type: Integer&lt;/li&gt;
&lt;li&gt;Required: true&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the other values can remain unchanged.&lt;/p&gt;

&lt;p&gt;Click Update to save.&lt;/p&gt;

&lt;p&gt;Next, choose the demo DB as the database resource and add the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;
  &lt;span class="n"&gt;comments&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, set the query argument to be "id" and the value to be &lt;code&gt;{{params.id}}&lt;/code&gt;, which connects the parameter of the task to the query argument of this query.&lt;/p&gt;

&lt;p&gt;To test, grab an ID from one of the approved comments on the dashboard, enter it into the ID field of the "Flag comment as abusive" task, and click the "Execute task" button.&lt;/p&gt;

&lt;p&gt;The previously approved comment will now be flagged, which you can verify by visiting the dashboard again.&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%2Fjilf231630pkjnprh2il.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%2Fjilf231630pkjnprh2il.jpg" alt="the comment moderation dashboard showing a manually flagged comment correctly moved to the flagged table" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a task to unflag comments
&lt;/h3&gt;

&lt;p&gt;Now, it's not against our site rules to dislike something, so that comment should be approved. Let's add another task to allow us to do that.&lt;/p&gt;

&lt;p&gt;Create a task called "comment_approve" with the following details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: Approve comment&lt;/li&gt;
&lt;li&gt;Description: Approve a comment for public display.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add a parameter called &lt;code&gt;id&lt;/code&gt; as an integer with the description, "The ID of the comment to approve".&lt;/p&gt;

&lt;p&gt;Next, set the demo DB as the database resource and add this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt;
  &lt;span class="n"&gt;comments&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt;
  &lt;span class="n"&gt;flagged&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For query arguments, add a new one called &lt;code&gt;id&lt;/code&gt; with the value of &lt;code&gt;{{params.id}}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use the same comment ID that you just flagged and execute the task. It will now be back on the approved list in the dashboard.&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%2F1v9ofbcoicmqfa84b8d1.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%2F1v9ofbcoicmqfa84b8d1.jpg" alt="the comment moderation dashboard showing the previously flagged comment back in the approved table after manual approval" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a SQL task to delete comments
&lt;/h3&gt;

&lt;p&gt;After an admin has reviewed a flagged comment to confirm that, yep, this comment is terrible, we want to let them delete it permanently — no reason for anyone else to have to see that trash.&lt;/p&gt;

&lt;p&gt;To do that, create a new task called "comment_delete" with the following details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: Delete comment permanently&lt;/li&gt;
&lt;li&gt;Description: Removes a comment permanently. There is no undo for this action!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add a parameter called &lt;code&gt;id&lt;/code&gt; as an integer with the description, "The ID of the comment to delete".&lt;/p&gt;

&lt;p&gt;Next, set the demo DB as the database resource and add this query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt;
  &lt;span class="n"&gt;comments&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For query arguments, add a new one called &lt;code&gt;id&lt;/code&gt; with the value of &lt;code&gt;{{params.id}}&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Test this by adding a comment ID in the field and clicking "execute task".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Remember that you can always run the "Reset comments database" task to revert the comments table to its starting state, so don't worry about deleting things.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Call tasks from table rows in Airplane
&lt;/h2&gt;

&lt;p&gt;At this point, what we've built is already pretty useful. We can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;View comments (both flagged and approved)&lt;/li&gt;
&lt;li&gt;Toggle the flagged status of comments&lt;/li&gt;
&lt;li&gt;Delete comments&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This &lt;em&gt;could&lt;/em&gt; be considered good enough. But we can make this much more user friendly with only a few more lines of code thanks to a built-in Airplane feature called &lt;a href="https://docs.airplane.dev/views/table#row-actions" rel="noopener noreferrer"&gt;row actions&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a row action to flag or approve a comment
&lt;/h3&gt;

&lt;p&gt;In Airplane &lt;code&gt;Table&lt;/code&gt; components, we can add a &lt;code&gt;rowActions&lt;/code&gt; prop that adds a button in each row and performs the specified action for the current row when clicked.&lt;/p&gt;

&lt;p&gt;There are a few ways to do this, up to and including fully custom solutions. For our needs, the &lt;a href="https://docs.airplane.dev/views/table#task-backed-row-actions" rel="noopener noreferrer"&gt;task-backed row actions&lt;/a&gt; are perfect: they will automatically pass through the current comment's ID — we only need to provide the task to be performed and label for it!&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;CommentModerationDashboard.airplane.ts&lt;/code&gt;, make the following changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;    import {
        type CheckboxState,
        Heading,
        Stack,
        Table,
        Checkbox,
        useComponentState,
    } from '@airplane/views';
    import airplane from 'airplane';
&lt;span class="err"&gt;
&lt;/span&gt;    const CommentModerationDashboard = () =&amp;gt; {
        const { id, checked } = useComponentState&amp;lt;CheckboxState&amp;gt;();
&lt;span class="err"&gt;
&lt;/span&gt;        return (
            &amp;lt;Stack&amp;gt;
                &amp;lt;Heading&amp;gt;Comment Moderation Dashboard&amp;lt;/Heading&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;                &amp;lt;Table
                    title="Approved Comments"
                    task="comments_list_approved"
                    defaultPageSize={20}
                    hiddenColumns={['flagged']}
&lt;span class="gi"&gt;+                   rowActions={{
+                       slug: 'comment_flag',
+                       label: 'flag',
+                   }}
&lt;/span&gt;                /&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;                &amp;lt;Checkbox
                    id={id}
                    label="Show flagged comments (view at your own risk!)"
                /&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;                {checked ? (
                    &amp;lt;Table
                        title="Flagged Comments"
                        task="comments_list_flagged"
                        defaultPageSize={20}
                        hiddenColumns={['flagged']}
&lt;span class="gi"&gt;+                       rowActions={[
+                           {
+                               slug: 'comment_approve',
+                               label: 'approve',
+                           },
+                           {
+                               slug: 'comment_delete',
+                               label: 'delete',
+                           },
+                       ]}
&lt;/span&gt;                    /&amp;gt;
                ) : null}
            &amp;lt;/Stack&amp;gt;
        );
    };
&lt;span class="err"&gt;
&lt;/span&gt;    export default airplane.view(
        {
            slug: 'comment_moderation_dashboard',
            name: 'Comment Moderation Dashboard',
            description:
                "Allows admins to see all approved comments, and optionally see flagged comments. They're also able to change the approved/flagged state of a comment and delete comments permanently.",
        },
        CommentModerationDashboard,
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save, refresh the studio, and your dashboard will now show the "flag" option on approved comments, and the "approve" and "delete" options on flagged comments.&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%2Fffepqiu63ur3hymwoqj5.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%2Fffepqiu63ur3hymwoqj5.jpg" alt="the comment moderation dashboard with row actions added to allow flagging, approving, and deleting comments" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Test out the buttons to see row actions in, uh, action!&lt;/p&gt;

&lt;h3&gt;
  
  
  Refresh other tables when row actions are performed
&lt;/h3&gt;

&lt;p&gt;You may have noticed that right now, the table that contains the updated row updates, but other tables require a page refresh to show the changes.&lt;/p&gt;

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

&lt;p&gt;Airplane &lt;a href="https://docs.airplane.dev/views/calling-tasks-with-react-hooks#usetaskquery" rel="noopener noreferrer"&gt;provides a hook called &lt;code&gt;useTaskQuery&lt;/code&gt;&lt;/a&gt; that lets us, among other things, force a refetch of the given task, causing all components using it to update.&lt;/p&gt;

&lt;p&gt;Make the following changes in &lt;code&gt;CommentModerationDashboard.airplane.ts&lt;/code&gt; to refetch all tables whenever a comment is modified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;    import {
        type CheckboxState,
        Heading,
        Stack,
        Table,
        Checkbox,
        useComponentState,
&lt;span class="gi"&gt;+       useTaskQuery,
&lt;/span&gt;    } from '@airplane/views';
    import airplane from 'airplane';
&lt;span class="err"&gt;
&lt;/span&gt;    const CommentModerationDashboard = () =&amp;gt; {
        const { id, checked } = useComponentState&amp;lt;CheckboxState&amp;gt;();
&lt;span class="gi"&gt;+       const flagged = useTaskQuery('comments_list_flagged');
+       const approved = useTaskQuery('comments_list_approved');
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;        return (
            &amp;lt;Stack&amp;gt;
                &amp;lt;Heading&amp;gt;Comment Moderation Dashboard&amp;lt;/Heading&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;                &amp;lt;Table
                    title="Approved Comments"
                    task="comments_list_approved"
                    defaultPageSize={20}
                    hiddenColumns={['flagged']}
                    rowActions={{
                        slug: 'comment_flag',
                        label: 'flag',
&lt;span class="gi"&gt;+                       onSuccess: () =&amp;gt; flagged.refetch(),
&lt;/span&gt;                    }}
                /&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;                &amp;lt;Checkbox
                    id={id}
                    label="Show flagged comments (view at your own risk!)"
                /&amp;gt;
&lt;span class="err"&gt;
&lt;/span&gt;                {checked ? (
                    &amp;lt;Table
                        title="Flagged Comments"
                        task="comments_list_flagged"
                        defaultPageSize={20}
                        hiddenColumns={['flagged']}
                        rowActions={[
                            {
                                slug: 'comment_approve',
                                label: 'approve',
&lt;span class="gi"&gt;+                               onSuccess: () =&amp;gt; approved.refetch(),
&lt;/span&gt;                            },
                            {
                                slug: 'comment_delete',
                                label: 'delete',
                            },
                        ]}
                    /&amp;gt;
                ) : null}
            &amp;lt;/Stack&amp;gt;
        );
    };
&lt;span class="err"&gt;
&lt;/span&gt;    export default airplane.view(
        {
            slug: 'comment_moderation_dashboard',
            name: 'Comment Moderation Dashboard',
            description:
                "Allows admins to see all approved comments, and optionally see flagged comments. They're also able to change the approved/flagged state of a comment and delete comments permanently.",
        },
        CommentModerationDashboard,
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save, then try again. Now both tables update whenever a comment is modified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a TypeScript task to add a new comment
&lt;/h2&gt;

&lt;p&gt;To this point, we've been using only SQL tasks, but &lt;a href="https://docs.airplane.dev/tasks/overview" rel="noopener noreferrer"&gt;Airplane also supports tasks written in JavaScript, Python, and more&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Create a new task and choose JavaScript as the type. Give it the name "comment_add" and leave TypeScript selected. Use the following details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: Add a comment&lt;/li&gt;
&lt;li&gt;Description: Save a new comment in the database. Includes a step to check for abusive comments and flag them to limit problematic content from becoming visible.&lt;/li&gt;
&lt;li&gt;Parameters: &lt;code&gt;comment&lt;/code&gt;, type "Long text"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add the demo DB as a resource, then open &lt;code&gt;comment_add.airplane.ts&lt;/code&gt; in your editor. The second argument to &lt;code&gt;airplane.task&lt;/code&gt; is an async function, which contains everything you want the task to do when called.&lt;/p&gt;

&lt;p&gt;Replace the boilerplate function with the following:&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="nx"&gt;airplane&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;airplane&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;airplane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;comment_add&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Add a comment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save a new comment in the database. Includes a step to check for abusive comments and flag them to limit problematic content from becoming visible.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;comment&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shorttext&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;resources&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;demo_db&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="k"&gt;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="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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// TODO add check for abusive content&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;flagged&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&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;airplane&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;demo_db&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;INSERT INTO comments (comment, flagged) VALUES (:comment, :flagged);&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;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;comment&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flagged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flagged&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`Your comment was saved.`&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="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;flagged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;flagged&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;Here's what this code does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get the &lt;code&gt;comment&lt;/code&gt; out of the &lt;code&gt;params&lt;/code&gt; so we can work with it&lt;/li&gt;
&lt;li&gt;For now, temporarily hard-code the output, which we'll build for real in the next section&lt;/li&gt;
&lt;li&gt;Use the built-in &lt;code&gt;airplane.sql.query&lt;/code&gt; method to run an &lt;code&gt;INSERT&lt;/code&gt; in our demo database to save the new comment along with its &lt;code&gt;flagged&lt;/code&gt; status&lt;/li&gt;
&lt;li&gt;Return a message and whether the comment was flagged&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save, then write a new comment in the task's input in the Studio and execute the task. Check the dashboard to see your new comment saved.&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%2F1bbqrggdgjmf0muz3bgw.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%2F1bbqrggdgjmf0muz3bgw.jpg" alt="the comment moderation dashboard showing a newly created comment in the approved table" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Use AI to automatically flag abusive comments
&lt;/h3&gt;

&lt;p&gt;Moderation is not optional when we're opening up spaces for public comments. Flagging abusive content is a must if you want to have a space free of harassment and other unacceptable behavior.&lt;/p&gt;

&lt;p&gt;The challenges with moderation are enormous and complicated. We won't cover all of them in this post, but we will look at two:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The people who moderate content prevent us from seeing the most terrible things that are said in the comments section — but for them to do their jobs, they have to read that awful content. A moderator is forced to confront the absolute worst the web has to offer every day, and that takes its toll.&lt;/li&gt;
&lt;li&gt;Good moderation means not showing comments until they've been checked. This adds a significant delay between posting a comment and seeing the comment live, which can prevent conversations from happening because it takes too long to see responses.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To address these two challenges, one possible solution is using a &lt;a href="https://en.wikipedia.org/wiki/Large_language_model" rel="noopener noreferrer"&gt;large language model (LLM)&lt;/a&gt; as a kind of "first line defense" for comment moderation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; Before we continue, let me add a giant, flashing asterisk to this section. &lt;strong&gt;AI is cool, but it has &lt;em&gt;huge&lt;/em&gt; unanswered ethical questions.&lt;/strong&gt; We haven't figured out how to control for the bias of the people training the models yet, and that means &lt;strong&gt;we need to be &lt;em&gt;extremely&lt;/em&gt; careful about how we use AI&lt;/strong&gt; — and we need to make sure we're checking its work thoroughly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this use case, we're attempting to catch abusive and hateful comments. We're also saving all comments for a final human review before deletion, which will allow us to adjust the instructions we're providing the LLM if it's incorrectly flagging comments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use Airplane's built-in AI functions to moderate user input
&lt;/h3&gt;

&lt;p&gt;Airplane provides several built-in operations, including &lt;a href="https://docs.airplane.dev/tasks/builtin-ai" rel="noopener noreferrer"&gt;AI support for both OpenAI and Anthropic&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For our app, we'll use &lt;a href="https://openai.com/" rel="noopener noreferrer"&gt;OpenAI&lt;/a&gt;, which currently provides $5 in credit to new accounts, which is more than enough to build and test this feature.&lt;/p&gt;

&lt;p&gt;Sign up or log in to your OpenAI account and &lt;a href="https://platform.openai.com/account/api-keys" rel="noopener noreferrer"&gt;create an API key&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next, head to the the Airplane Studio and create a config var (the icon that looks like &lt;code&gt;(x)&lt;/code&gt; in the left-hand sidebar). Name the config &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; and paste in your OpenAI key.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; This value will be added to &lt;code&gt;airplane.dev.yaml&lt;/code&gt;! Remember not to commit this file to avoid leaking your API key.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Go back to your "Add a comment" task in the explorer and scroll down to the "build" section of the config. Under "Environment variables", click "add variable". Name it &lt;code&gt;OPENAI_API_KEY&lt;/code&gt;, then choose "From config var" from the dropdown. In the new dropdown that appears, choose the &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; config.&lt;/p&gt;

&lt;p&gt;This will update the task file with a reference to the config var, which makes it safe to commit the task file (but, again, &lt;em&gt;do not&lt;/em&gt; commit &lt;code&gt;airplane.dev.yaml&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;With the API key available, modify &lt;code&gt;comment_add.airplane.ts&lt;/code&gt; to add the AI moderation step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;    import airplane from 'airplane';
&lt;span class="err"&gt;
&lt;/span&gt;    export default airplane.task(
        {
            slug: 'comment_add',
            name: 'Add a comment',
            description:
                'Save a new comment in the database. Includes a step to check for abusive comments and flag them to limit problematic content from becoming visible.',
            parameters: {
                comment: {
                    name: 'comment',
                    type: 'shorttext',
                },
            },
            resources: ['demo_db'],
            envVars: {
                OPENAI_API_KEY: {
                    config: 'OPENAI_API_KEY',
                },
            },
        },
        async (params) =&amp;gt; {
            const { comment } = params;
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-           // TODO add check for abusive content
-           const output = { flagged: false };
&lt;/span&gt;&lt;span class="gi"&gt;+           const getSentiment = airplane.ai.func(
+               'Identify abusive and vulgar comments. Negative opinions are allowed but personal attacks are not.',
+               [
+                   {
+                       input: 'This is the shit!',
+                       output: { flagged: false, sentiment: 'positive' },
+                   },
+                   {
+                       input: 'You are stupid!',
+                       output: { flagged: true, sentiment: 'negative' },
+                   },
+                   {
+                       input: 'Burgers are gross',
+                       output: { flagged: false, sentiment: 'negative' },
+                   },
+               ],
+           );
+
+           const { output, confidence } = await getSentiment(comment);
+
+           if (typeof output === 'string') {
+               return { message: 'unparseable input' };
+           }
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;            const res = await airplane.sql.query(
                'demo_db',
                'INSERT INTO comments (comment, flagged) VALUES (:comment, :flagged);',
                { args: { comment, flagged: output.flagged } },
            );
&lt;span class="err"&gt;
&lt;/span&gt;            console.log(res);
&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;-           let message = `Your comment was saved.`;
&lt;/span&gt;&lt;span class="gi"&gt;+           let message = `Your comment was saved. The sentiment was read as ${output.sentiment}.`;
+           if (output.flagged === true &amp;amp;&amp;amp; confidence &amp;gt;= 0.75) {
+               message = 'Wow, you kiss your mother with that mouth?';
+           }
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;            return { message, flagged: output.flagged };
        },
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save, then add an abusive comment to make sure it'll get caught (I recommend trying, "you're a doofus").&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%2Fop8rkx26u3aqlb2kg9z9.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%2Fop8rkx26u3aqlb2kg9z9.jpg" alt="the add a comment task showing a result that was flagged, including the custom message we coded earlier" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Less than 25 lines of code, and we've got a pretty okay auto-moderation flow in place — that's pretty impressive!&lt;/p&gt;

&lt;p&gt;And again: remember that this is not a &lt;em&gt;replacement&lt;/em&gt; for human moderation. It's a tool that can help the moderators work more effectively.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy your Airplane dashboard
&lt;/h2&gt;

&lt;p&gt;So far we've only been working locally. To make this dashboard available to your team, you'll need to deploy it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add config vars for production
&lt;/h3&gt;

&lt;p&gt;For your tasks to work properly, you'll need to &lt;a href="https://app.airplane.dev/settings/config-vars" rel="noopener noreferrer"&gt;add a production config var&lt;/a&gt; with your OpenAI API key. Name it &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; — save it and you're all set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploy Airplane tasks and views to production
&lt;/h3&gt;

&lt;p&gt;To deploy, open the terminal and run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;airplane deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the deployment completes, head to &lt;a href="https://app.airplane.dev" rel="noopener noreferrer"&gt;https://app.airplane.dev&lt;/a&gt; and check the library to see your deployed tasks and views.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try the demo app to see the comment moderation flow in action
&lt;/h2&gt;

&lt;p&gt;To provide a way to test the moderation workflow, the demo app for this project &lt;a href="https://docs.airplane.dev/api/tasks" rel="noopener noreferrer"&gt;executes Airplane tasks directly via API&lt;/a&gt;. You could also set up a &lt;a href="https://docs.airplane.dev/tasks/webhooks" rel="noopener noreferrer"&gt;webhook&lt;/a&gt; to run the moderation flow in the background after a comment is added to your production database.&lt;/p&gt;

&lt;p&gt;We won't go through exactly how to build this demo app, but the overview is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's an Astro site&lt;/li&gt;
&lt;li&gt;The site runs in hybrid mode so it can process form submissions&lt;/li&gt;
&lt;li&gt;Approved comments are loaded by &lt;a href="https://docs.airplane.dev/api/tasks#tasks-execute" rel="noopener noreferrer"&gt;executing&lt;/a&gt; the &lt;code&gt;comments_list_approved&lt;/code&gt; task and &lt;a href="https://docs.airplane.dev/api/runs#runs-getOutputs" rel="noopener noreferrer"&gt;getting its outputs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;New comments are created by executing the &lt;code&gt;comment_add&lt;/code&gt; task and returning its outputs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To see the source code, &lt;a href="https://github.com/learnwithjason/airplane-content-moderation/tree/main/example-app" rel="noopener noreferrer"&gt;check out the GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For a user on the site, submitting a comment will take a few seconds and then immediate feedback will be sent: either the comment is approved and visible immediately, or it's flagged and the comment form will chide the user for posting abusive or vulgar comments.&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%2Fggm2m1m0hdshe1jxumjd.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%2Fggm2m1m0hdshe1jxumjd.jpg" alt="two frame flow of an acceptable comment being posted and receiving a confirmation message" width="800" height="450"&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%2Fr6b5tqgonkjmcjrp6kk2.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%2Fr6b5tqgonkjmcjrp6kk2.jpg" alt="two frame flow of an unacceptable comment that is submitted, but not displayed. instead, the user sees a message letting them know the comment was not posted" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Congratulations! You've built a complete comment moderation dashboard, including a first line of defense against the most vulgar and abusive comments powered by OpenAI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lwj.dev/newsletter" class="ltag_cta ltag_cta--branded" rel="noopener noreferrer"&gt;For more content like this, subscribe to my newsletter!&lt;/a&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Links and additional resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Source code: &lt;a href="https://github.com/learnwithjason/airplane-content-moderation" rel="noopener noreferrer"&gt;https://github.com/learnwithjason/airplane-content-moderation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/learnwithjason/airplane-content-moderation" rel="noopener noreferrer"&gt;Get started with Airplane&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;More information on the &lt;a href="https://hai.stanford.edu/news/timnit-gebru-ethical-ai-requires-institutional-and-structural-change" rel="noopener noreferrer"&gt;ethical concerns around AI&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A reminder that &lt;a href="https://www.scientificamerican.com/article/racial-bias-found-in-a-major-health-care-risk-algorithm/" rel="noopener noreferrer"&gt;biases in AI can quite literally kill people&lt;/a&gt; if we don’t actively check the results&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>tutorial</category>
      <category>ai</category>
      <category>react</category>
    </item>
    <item>
      <title>Clean as you go (a coding life hack)</title>
      <dc:creator>Jason Lengstorf</dc:creator>
      <pubDate>Fri, 21 Jul 2023 17:08:02 +0000</pubDate>
      <link>https://dev.to/jlengstorf/clean-as-you-go-a-coding-life-hack-3mf1</link>
      <guid>https://dev.to/jlengstorf/clean-as-you-go-a-coding-life-hack-3mf1</guid>
      <description>&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/a7_kghlRfeI"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;I learned to cook in a restaurant. I was a prep cook.&lt;/p&gt;

&lt;p&gt;Every morning, I clocked in to stacks of produce I needed to have ready before the lunch rush. Every morning, I grabbed a cutting board and a knife, opened the first box of romaine lettuce, and got to chopping. And every morning, I'd make an &lt;em&gt;enormous&lt;/em&gt; mess.&lt;/p&gt;

&lt;p&gt;As I chopped, I pushed the scraps aside into a pile to deal with them later. Cleaning, after all, happened &lt;em&gt;after&lt;/em&gt; I finished prep.&lt;/p&gt;

&lt;h2&gt;
  
  
  Big messes require big efforts to clean
&lt;/h2&gt;

&lt;p&gt;But the scraps piled up, and my workspace got tighter and tighter. Onion skins stuck to the tomatoes; remnants of diced parsley speckled the cucumbers. My scrap pile got so out of control that I'd occasionally end up dumping handfuls of veggie waste onto the floor by mistake.&lt;/p&gt;

&lt;p&gt;Working around my mess made prep stressful, but that paled in comparison to the cleanup. My prep technique left the kitchen looking like the aftermath of a food fight. It took me so long to clean up that I had to rush to avoid delaying the lunch shift from getting started. (There should have been an hour or so of downtime.)&lt;/p&gt;

&lt;p&gt;The more experienced staff was horrified by this.&lt;/p&gt;

&lt;p&gt;And, fortunately for me, they decided to help me instead of just firing me immediately.&lt;/p&gt;

&lt;h2&gt;
  
  
  Clean as you go to avoid big messes
&lt;/h2&gt;

&lt;p&gt;One of the cooks very patiently introduced me to the concept of cleaning as you go. They had a bench scraper and a bleach bucket standing by, and positioned the trash can right next to the table. As I chopped, they encouraged me to move the scraps directly to the trash instead of making a pile. When I finished an ingredient, I grabbed the bench scraper to clear away all the left behind bits, then gave a quick wipe with a rag to reset my station to clean before starting the next one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; If you cook, I can't recommend getting a &lt;a href="https://www.amazon.com/OXO-Multi-purpose-Stainless-Scraper-Chopper/dp/B00004OCNJ/" rel="noopener noreferrer"&gt;bench scraper&lt;/a&gt; enough. Use it to scoop ingredients off the cutting board instead of using your knife (and potentially dulling it), get all the bits off your flat surfaces, cut and measure dough — it's a versatile tool that I've noticed people don't tend to know about unless they've worked in restaurant kitchens before.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At first, I resisted this. Cleaning at each step &lt;em&gt;had&lt;/em&gt; to be slower, right? After all, &lt;a href="https://www.jason.energy/context-switching/" rel="noopener noreferrer"&gt;context switching is a terrible idea&lt;/a&gt;, and this was clearly going to make me even slower for sure.&lt;/p&gt;

&lt;p&gt;But my options were to do it their way or get fired, so I stuck to it. And a few things happened:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The quality of my work went up. No more bits of the previous veggie showing up in the next one. And because my workspace was less cramped, I could see what I was doing and make fine adjustments more easily.&lt;/li&gt;
&lt;li&gt;The prep itself went faster. Starting with a clean station made it faster to do the prep because I wasn't working around previous mess.&lt;/li&gt;
&lt;li&gt;Cleanup went faster. By cleaning as I went, cleaning was as simple as a few swipes with a bench scraper and a quick wipe-down with a rag. Nothing piled up, so my last veggie was as easy to clean up after as the first one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I wasn't rushing to get everything done and cleaned anymore. Instead of the cooks worrying about me, they started giving me additional training and opportunities.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It felt counterintuitive, but cleaning as I went had made me both &lt;em&gt;better&lt;/em&gt; and &lt;em&gt;faster&lt;/em&gt; at my job.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Isn't stopping to clean slower?
&lt;/h2&gt;

&lt;p&gt;My knee-jerk reaction to cleaning as I went was to condemn it as multitasking, which makes work slower. But... was it?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; I'm taking a bit of artistic license with &lt;br&gt;
 timelines to connect this story. I learned about cleaning as you go in my teens; context switching didn't formally enter my awareness until a decade or so later. With the benefit of hindsight, I &lt;em&gt;think&lt;/em&gt; my initial distaste for cleaning as you go was some vague sense of "it'll take longer!" — but honestly I was a teenager so I may have just been against it because I was acting like a sulky prick.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Cleaning as you go &lt;em&gt;seems&lt;/em&gt; like multitasking, but it's actually an efficient way of sequentially tackling work.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Small messes clean up fast
&lt;/h2&gt;

&lt;p&gt;By &lt;em&gt;not&lt;/em&gt; cleaning as we go, multiple messes compound on each other. A small mess is fast to clean, but a dozen or more small messes become a much bigger mess that's harder to clean up.&lt;/p&gt;

&lt;p&gt;In food prep, the scrap pile might spill off the table. Wet scraps might dry to the table, making them far more difficult to clean up. A large pile has a tendency to spread out and get additional dishes and utensils dirty.&lt;/p&gt;

&lt;p&gt;By contrast, a small mess is almost always a couple quick wipes away from being clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Clean as you go" as a guiding principle for everything
&lt;/h2&gt;

&lt;p&gt;This lesson didn't just save my high school job — it made a huge impression on my outlook on life. I try to clean as I go wherever I can these days, whether I'm cooking dinner at home or writing code at work.&lt;/p&gt;

&lt;p&gt;In a codebase, we always make small messes as we work. We're solving problems, and until we hit a solution that works our code is full of small choices based on educated guesses that are &lt;em&gt;directionally&lt;/em&gt; correct, but that probably wander a bit since they lack the full context of the working solution.&lt;/p&gt;

&lt;p&gt;If we ship the code as-is, everything will be fine. The code works, and that's ultimately the point.&lt;/p&gt;

&lt;p&gt;But we left a small mess. And if we do that every time we build a new feature, the small messes start to build into a big mess — this is how we end up with tech debt that's so overwhelming that we start to argue that the only reasonable solution is to start over from scratch.&lt;/p&gt;

&lt;p&gt;If, instead, we choose to clean as we go — to do an immediate small refactor as part of submitting the PR, for example — we'll clean up those small messes &lt;em&gt;before&lt;/em&gt; they can pile up into big ones. And because the messes are being continually addressed, we have cleaner, easier to manage code, and that makes us faster.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building new habits is hard, but worth it
&lt;/h2&gt;

&lt;p&gt;Cleaning as you go is a tricky habit to build. It requires a concentrated effort to change the way you work, and if you're in a team it's even trickier because you're trying to shift the team culture.&lt;/p&gt;

&lt;p&gt;However, if you put in the work to build the habit, it pays dividends in every area of your life where you choose to adopt it.&lt;/p&gt;

</description>
      <category>productivity</category>
      <category>career</category>
      <category>learning</category>
      <category>devjournal</category>
    </item>
    <item>
      <title>Be weird &amp; fun (as a business strategy)</title>
      <dc:creator>Jason Lengstorf</dc:creator>
      <pubDate>Wed, 01 Jun 2022 17:14:24 +0000</pubDate>
      <link>https://dev.to/jlengstorf/be-weird-fun-as-a-business-strategy-58l1</link>
      <guid>https://dev.to/jlengstorf/be-weird-fun-as-a-business-strategy-58l1</guid>
      <description>&lt;p&gt;The short version: &lt;strong&gt;I worked with my incredible team to write and create a video comparing the experience of Netlify with other development workflows.&lt;/strong&gt; We shot this with a professional production team in a studio in San Francisco and I think it's almost as fun to watch as it was for us to make it.&lt;/p&gt;

&lt;p&gt;Watch it here:&lt;/p&gt;

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

&lt;p&gt;For more information, you can also &lt;a href="https://netlify.com/blog/making-tale-of-web-development-in-two-universes/?utm_source=jason-af&amp;amp;utm_medium=brand-video-0422-meta&amp;amp;utm_campaign=devex-jl" rel="noopener noreferrer"&gt;read the official Netlify post about this video&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why make a video like this?
&lt;/h2&gt;

&lt;p&gt;The post-pandemic world has changed how people want to spend their time. Things that used to be a staple, like webinars, have become much harder to attract attendees to because &lt;strong&gt;no one wants yet another Zoom call on their calendar.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We can't just go back to only in-person events, either, because not everyone is ready to travel or be in a crowded room again. Many people are being choosy about what they're willing to travel for — they're (quite reasonably) prioritizing vacations and family visits over company events and conferences.&lt;/p&gt;

&lt;p&gt;Reaching folks in the community is more challenging than ever. We need to get creative — the old ways of outreach are showing diminishing returns. Companies could see this as a sign of doom, but I find it pretty exciting: it means that &lt;strong&gt;we're not able to rely on the standard playbook anymore&lt;/strong&gt;, so companies are incentivized to try new things. That means some of my weirder ideas that might have been dismissed as too risky in the past might actually get greenlit now.&lt;/p&gt;

&lt;p&gt;Here are some loosely structured thoughts on why I think this project was a good way to shake things up and reach the broader community.&lt;/p&gt;

&lt;h2&gt;
  
  
  Showing how it feels to use something is easier than writing it down
&lt;/h2&gt;

&lt;p&gt;This project is our attempt to create something fun to watch that tells a story that's been hard for us to tell: Netlify falls into that category of products where the real value of what we're building is &lt;em&gt;how it feels to use it&lt;/em&gt; — &lt;strong&gt;we solve real problems for teams that build for the web, and once they've switched to our tools they &lt;em&gt;feel&lt;/em&gt; like they've gained superpowers.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We know that people who take advantage of &lt;a href="https://www.netlify.com?utm_source=jason-af&amp;amp;utm_medium=brand-video-0422-meta&amp;amp;utm_campaign=devex-jl" rel="noopener noreferrer"&gt;Netlify's platform for modern web development&lt;/a&gt; will love it — we've seen that over and over again. But how do we convince people to try it in the first place?&lt;/p&gt;

&lt;p&gt;In my experience, when I talk to folks who haven't tried Netlify yet, it's not because they're happy with how they work today. It's often because they weren't aware that they had a choice of something better. Especially in bigger companies, people expect things to be bureaucratic and hard — that's just Big Business™, right?&lt;/p&gt;

&lt;p&gt;So when I tell them that huge companies like Unilever, &lt;a href="https://www.netlify.com/blog/2019/02/27/featured-site-nike-just-do-it/?utm_source=jason-af&amp;amp;utm_medium=brand-video-0422-meta&amp;amp;utm_campaign=devex-jl" rel="noopener noreferrer"&gt;Nike&lt;/a&gt;, &lt;a href="https://www.netlify.com/blog/2021/11/08/twilio-console-a-large-scale-migration-to-jamstack/?utm_source=jason-af&amp;amp;utm_medium=brand-video-0422-meta&amp;amp;utm_campaign=devex-jl" rel="noopener noreferrer"&gt;Twilio&lt;/a&gt;, and Verizon have switched to Netlify, there's this dawning of realization that &lt;em&gt;they have options&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This video is an attempt to create something interesting enough to watch on its own, and along the way show all the folks I haven't had the chance to talk to in person that &lt;em&gt;they have options, too&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you've got a talented team, you should play to your strengths
&lt;/h2&gt;

&lt;p&gt;Our team at Netlify is absolutely stacked with creative, talented people. &lt;a href="https://marisamorby.com/" rel="noopener noreferrer"&gt;Marisa&lt;/a&gt; on our Design Research team has been performing since she was a toddler. &lt;a href="https://www.linkedin.com/in/rachael-stavchansky/" rel="noopener noreferrer"&gt;Rachael&lt;/a&gt;, our Senior Manager of Technical Writing for &lt;a href="https://docs.netlify.com/?utm_source=jason-af&amp;amp;utm_medium=brand-video-0422-meta&amp;amp;utm_campaign=devex-jl" rel="noopener noreferrer"&gt;Developer Documentation&lt;/a&gt;, is not only a great performer but actually worked as a teacher for theater. Our Director of Developer Experience, &lt;a href="https://www.hawksworx.com/" rel="noopener noreferrer"&gt;Phil&lt;/a&gt;, is a stand-up comedian on the side. That's just a sampling of the team — just about everyone has a broad, fascinating background that gives them experience in the performing arts or other creative outlets.&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%2F0jm8pu73jq1fny0rhzzc.gif" 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%2F0jm8pu73jq1fny0rhzzc.gif" alt="Tara popping up from behind the chair in the video" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We've assembled a team that is willing to go all-in on a wild idea and have a blast trying something new, and to not take advantage of that feels like a huge missed opportunity.&lt;/p&gt;

&lt;p&gt;When we wrote the script, we put in opportunities for the team to help build the worlds in these two universes. &lt;strong&gt;Many of the one-liners and gags in the video are the results of the team doing improv on the spot&lt;/strong&gt; — &lt;a href="https://twitter.com/tzmanics" rel="noopener noreferrer"&gt;Tara&lt;/a&gt;, our Manager of Templates Engineering, stole the show with her portrayal of the "other universe boss".&lt;/p&gt;

&lt;p&gt;I had so much dang fun working on this video with this delightful group of weirdos. &lt;/p&gt;

&lt;h2&gt;
  
  
  Whatever you do, lead with fun
&lt;/h2&gt;

&lt;p&gt;If you're a developer, you've probably watched quite a bit of video about web development. If you try to recall your favorites, what comes to mind?&lt;/p&gt;

&lt;p&gt;For me, I think of the things that made me laugh while teaching me something:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cassidy Williams &lt;a href="https://www.tiktok.com/@cassidoo/video/6920389224902561029?is_from_webapp=1&amp;amp;sender_device=pc&amp;amp;web_id=7080690333706094123" rel="noopener noreferrer"&gt;teaching us empathy for OSS devs&lt;/a&gt; on TikTok&lt;/li&gt;
&lt;li&gt;Anjana Vakil and Natalia Margolis teaching &lt;a href="https://youtu.be/-PX0BV9hGZY" rel="noopener noreferrer"&gt;Tail Call Optimization as a musical&lt;/a&gt; at !!Con&lt;/li&gt;
&lt;li&gt;Gary Bernhardt's classic speedrun of &lt;a href="https://www.destroyallsoftware.com/talks/wat" rel="noopener noreferrer"&gt;weird behavior in programming languages, "Wat"&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The majority of the content out there is information dense, but dry.&lt;/strong&gt; I often find myself &lt;em&gt;forcing&lt;/em&gt; my way through a video because I need the information, but I'm certainly not enjoying myself. It feels like a chore to get the knowledge out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://frontendmasters.com/courses/serverless-functions/" rel="noopener noreferrer"&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%2Flysu8v7770hq52kuv1b6.png" alt="Screenshot of a project from Jason's Frontend Masters Serverless course, which features four movie posters photoshopped with corgis in place of the actors." width="800" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finding ways to make knowledge transfer fun has been pretty central to my career — whether it's something like &lt;a href="https://www.learnwithjason.dev" rel="noopener noreferrer"&gt;&lt;em&gt;Learn With Jason&lt;/em&gt;&lt;/a&gt; where I'm actively teaching development skills, or the way I entertain myself with meta-commentary in &lt;a href="https://www.jason.af/yak-shaving/" rel="noopener noreferrer"&gt;my blog posts&lt;/a&gt;, or the absurd level of effort I put into &lt;a href="https://frontendmasters.com/courses/serverless-functions/" rel="noopener noreferrer"&gt;silly apps for my workshops&lt;/a&gt; because they make me smile.&lt;/p&gt;

&lt;p&gt;I can only speak for myself, but &lt;strong&gt;I am &lt;em&gt;far&lt;/em&gt; more likely to follow through on something if I'm having fun while I do it, so I've started prioritizing fun in projects&lt;/strong&gt; as the most important factor. No matter what we make, we're going to share knowledge; that's table stakes. When we look at &lt;em&gt;how&lt;/em&gt; we're going to share that knowledge, the approach that sounds like the most fun is going to win out every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's more where this came from
&lt;/h2&gt;

&lt;p&gt;We've been working toward this video for over a year now. It takes a lot of coordination with a lot of people to start something new like this: we have to get aligned on strategy, agree on budgets, make sure we're telling the right story, hook into our overarching strategy, and a million other little agreements that need to be in place for something like this to exist.&lt;/p&gt;

&lt;p&gt;I'm making a big assumption that this video will be successful in the ways we want it to be. But unless things go pretty poorly, I'll keep pushing for weird, fun projects like these. (Which I guess is a good opportunity for me to say, if this sounds like your kind of fun, &lt;a href="https://www.netlify.com/careers/?utm_source=brand-video-0422-meta&amp;amp;utm_medium=blog&amp;amp;utm_campaign=devex-jl" rel="noopener noreferrer"&gt;we're hiring&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;I'm excited to explore new ideas and media. I hope you'll have as much fun watching what we're able to create as we're having making it.&lt;/p&gt;

</description>
      <category>creativity</category>
      <category>netlify</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Send GraphQL Queries With the Fetch API (Without Apollo, URQL)</title>
      <dc:creator>Jason Lengstorf</dc:creator>
      <pubDate>Tue, 22 Dec 2020 17:28:20 +0000</pubDate>
      <link>https://dev.to/netlify/send-graphql-queries-with-the-fetch-api-without-apollo-urql-30n4</link>
      <guid>https://dev.to/netlify/send-graphql-queries-with-the-fetch-api-without-apollo-urql-30n4</guid>
      <description>&lt;p&gt;GraphQL is a powerful solution for working with data, but it often gets a bad rap for being too complicated to set up and use. In this tutorial, we'll learn how to send GraphQL queries and mutations without any third-party tools using the browser's built-in Fetch API.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to send a GraphQL query with Fetch
&lt;/h2&gt;

&lt;p&gt;Under the hood, GraphQL works by sending HTTP requests to an endpoint. This means that there's nothing magical about sending a GraphQL request — we can use built-in browser APIs to send them!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; We'll be using the built-in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch" rel="noopener noreferrer"&gt;Fetch API&lt;/a&gt; for this example, but you could also use &lt;a href="https://github.com/axios/axios" rel="noopener noreferrer"&gt;axios&lt;/a&gt;, &lt;a href="https://api.jquery.com/jquery.ajax/" rel="noopener noreferrer"&gt;jQuery.ajax()&lt;/a&gt;, or your preferred library for sending HTTP requests.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;First, let's look at the component parts of a GraphQL query:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The query itself&lt;/li&gt;
&lt;li&gt;Any query variables&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A GraphQL query might look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;GetLearnWithJasonEpisodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DateTime&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;allEpisode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ASC&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;gte&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$now&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;guest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;twitter&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query loads the &lt;a href="https://www.learnwithjason.dev" rel="noopener noreferrer"&gt;&lt;em&gt;Learn With Jason&lt;/em&gt;&lt;/a&gt; schedule by searching for all episodes with a date greater than &lt;code&gt;$now&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But what is &lt;code&gt;$now&lt;/code&gt;? A query variable!&lt;/p&gt;

&lt;p&gt;Query variables are passed to GraphQL as a JavaScript object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, the variable will be set to the current date and time that the query is executed, which means we'll only see future episodes.&lt;/p&gt;

&lt;h2&gt;
  
  
  How can we send the GraphQL query to the GraphQL endpoint using Fetch?
&lt;/h2&gt;

&lt;p&gt;Once we have the query and variables, we can write a bit of JavaScript to send a call with the Fetch API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://www.learnwithjason.dev/graphql&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
        query GetLearnWithJasonEpisodes($now: DateTime!) {
          allEpisode(limit: 10, sort: {date: ASC}, where: {date: {gte: $now}}) {
            date
            title
            guest {
              name
              twitter
            }
            description
          }
        }
      `&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&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;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sends the GraphQL query and variables as a JSON object to the endpoint &lt;code&gt;https://www.learnwithjason.dev/graphql&lt;/code&gt;, then logs the result, which looks 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%2Fwww.netlify.com%2Fimg%2Fblog%2Fgraphql-fetch-result.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%2Fwww.netlify.com%2Fimg%2Fblog%2Fgraphql-fetch-result.png" alt="screenshot of GraphQL query result in the console after sending a fetch request" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; You can open your browser console and run this JavaScript to see this in action &lt;em&gt;right now!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you'd like to try this with other GraphQL endpoints, check out the &lt;a href="https://rickandmortyapi.com/documentation/#graphql" rel="noopener noreferrer"&gt;Rick and Morty GraphQL API&lt;/a&gt; or the &lt;a href="https://countries.trevorblades.com/" rel="noopener noreferrer"&gt;countries API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are the requirements to send a GraphQL query request?
&lt;/h2&gt;

&lt;p&gt;For a GraphQL request to be successfully sent as an HTTP request, we have to meet a few requirements. Let's step through them one at a time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The request needs to be sent using the &lt;code&gt;POST&lt;/code&gt; method
&lt;/h3&gt;

&lt;p&gt;Some endpoints may support other methods, but I have yet to find one that doesn’t support &lt;code&gt;POST&lt;/code&gt;, so it's a safe bet to use this with any GraphQL endpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  The query and variables need to be sent as a JSON object
&lt;/h3&gt;

&lt;p&gt;GraphQL endpoints expect the &lt;code&gt;body&lt;/code&gt; of the request to be a stringified JSON object that contains a &lt;code&gt;query&lt;/code&gt; and &lt;code&gt;variables&lt;/code&gt; property.&lt;/p&gt;

&lt;p&gt;Even if you don't have variables, send an empty object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`
    query SomeQuery {
      # your query here
    }`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Send the right headers
&lt;/h3&gt;

&lt;p&gt;This is optional, technically, but it's a good idea to include a &lt;code&gt;Content-Type&lt;/code&gt; header to specify that you're sending JSON.&lt;/p&gt;

&lt;p&gt;Many GraphQL endpoints will require an &lt;code&gt;Authorization&lt;/code&gt; header or other access control, which will vary depending on the service or tools you're using to serve GraphQL data. Check the docs for your GraphQL endpoint if you run into access control issues when sending your request.&lt;/p&gt;

&lt;h2&gt;
  
  
  GraphQL clients are powerful, but you may not need one!
&lt;/h2&gt;

&lt;p&gt;GraphQL clients like Apollo and URQL add a lot of extra power, including caching support and advanced features like subscriptions. In apps that have lots of queries or that are implementing complex data management, it's probably a good idea to implement an actual GraphQL client.&lt;/p&gt;

&lt;p&gt;However, if you're building an app that needs to make a few GraphQL queries, you may not need a full-blown GraphQL client! In a lot of cases, a simple HTTP request is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://graphql.org/learn/" rel="noopener noreferrer"&gt;Learn more about GraphQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch" rel="noopener noreferrer"&gt;Learn more about the Fetch API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Watch me and Emma Bostian use this approach to &lt;a href="https://www.learnwithjason.dev/we-need-to-taco-bout-your-choices" rel="noopener noreferrer"&gt;build a dynamic Jamstack app with Hasura GraphQL&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>graphql</category>
      <category>javascript</category>
      <category>fetch</category>
    </item>
    <item>
      <title>Access Query String Parameters in Serverless Functions</title>
      <dc:creator>Jason Lengstorf</dc:creator>
      <pubDate>Sat, 11 Jan 2020 01:47:28 +0000</pubDate>
      <link>https://dev.to/jlengstorf/access-query-string-parameters-in-serverless-functions-338b</link>
      <guid>https://dev.to/jlengstorf/access-query-string-parameters-in-serverless-functions-338b</guid>
      <description>&lt;p&gt;Serverless functions really start to show their potential when we can accept user input and respond to it. The most straightforward way to do this is with query parameters in the browser using &lt;code&gt;GET&lt;/code&gt; requests. In this article, we'll look at how to retrieve query parameters in a serverless function and use them to affect the output of our function.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; This tutorial uses &lt;a href="https://www.netlify.com/products/functions/?utm_source=learnwithjason&amp;amp;utm_medium=sls-query-string-jl&amp;amp;utm_campaign=devex" rel="noopener noreferrer"&gt;Netlify Functions&lt;/a&gt; for development and deployment. If you'd like more information, I've written a &lt;a href="https://www.learnwithjason.dev/blog/serverless-functions/deploy-first-serverless-function/" rel="noopener noreferrer"&gt;primer on how to deploy serverless functions to Netlify&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Create your dev environment
&lt;/h2&gt;

&lt;p&gt;If you've been following along with the &lt;a href="https://www.learnwithjason.dev/blog/serverless-functions/overview/" rel="noopener noreferrer"&gt;full series on serverless functions&lt;/a&gt;, you can use the same codebase you originally set up. If you're starting fresh, you can clone &lt;a href="https://github.com/jlengstorf/serverless-functions/tree/starter" rel="noopener noreferrer"&gt;the starter repo&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# clone the starter branch of the repo&lt;/span&gt;
git clone &lt;span class="nt"&gt;--single-branch&lt;/span&gt; &lt;span class="nt"&gt;--branch&lt;/span&gt; starter https://github.com/jlengstorf/serverless-functions.git

&lt;span class="c"&gt;# move into the project&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;serverless-functions/

&lt;span class="c"&gt;# install dependencies (can also use `npm install`)&lt;/span&gt;
yarn

&lt;span class="c"&gt;# install the Netlify CLI (can also use npm i -g netlify-cli)&lt;/span&gt;
yarn global add netlify-cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE:&lt;/strong&gt; check out the &lt;a href="https://docs.netlify.com/cli/get-started/?utm_source=learnwithjason&amp;amp;utm_medium=sls-query-string-jl&amp;amp;utm_campaign=devex" rel="noopener noreferrer"&gt;Netlify CLI docs&lt;/a&gt; for more details.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Write the serverless function
&lt;/h2&gt;

&lt;p&gt;To start, let’s create a serverless function that will allow us to boop our friends. Create a file called &lt;code&gt;boop-a-friend.js&lt;/code&gt; in the &lt;code&gt;functions&lt;/code&gt; folder of your project and write the following code inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// TODO get this value from the query string&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;boopee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Jason&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`You booped &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;boopee&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; on the nose. Boop!`&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;Run &lt;code&gt;netlify dev&lt;/code&gt; and visit &lt;code&gt;http://localhost:9999/.netlify/functions/boop-a-friend&lt;/code&gt; and we see the return text, "You booped Jason on the nose. Boop!"&lt;/p&gt;

&lt;p&gt;We want to be able to choose who we boop, though, and we want to do that from the browser — meaning we'll be using the &lt;code&gt;GET&lt;/code&gt; method to call our serverless function. In practice, it will look 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;http://localhost:9999/.netlify/functions/boop-a-friend?boopee=Marisa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're using a &lt;a href="https://en.wikipedia.org/wiki/Query_string" rel="noopener noreferrer"&gt;query string&lt;/a&gt; (the &lt;code&gt;?boopee=Marisa&lt;/code&gt; part) to set our &lt;code&gt;boopee&lt;/code&gt; value to "Marisa", because that's who we want to boop.&lt;/p&gt;

&lt;p&gt;Next, we need to use the query string value in our serverless function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retrieve values from query strings in serverless functions
&lt;/h2&gt;

&lt;p&gt;Because we're using &lt;a href="https://www.netlify.com/products/functions/?utm_source=learnwithjason&amp;amp;utm_medium=sls-query-string-jl&amp;amp;utm_campaign=devex" rel="noopener noreferrer"&gt;Netlify Functions&lt;/a&gt;, we receive the &lt;code&gt;event&lt;/code&gt; as the first argument to our serverless function, and it contains a property called &lt;code&gt;queryStringParameters&lt;/code&gt;, which contains any query string values as an object.&lt;/p&gt;

&lt;p&gt;What this looks like in practice is that if we call our serverless function using the query string above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:9999/.netlify/functions/boop-a-friend?boopee=Marisa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can access query parameters in our serverless function by making the following changes:&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="gd"&gt;- exports.handler = async () =&amp;gt; {
&lt;/span&gt;&lt;span class="gi"&gt;+ exports.handler = async event =&amp;gt; {
+   console.log(event.queryStringParameters);
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    // TODO get this value from the query string
    const boopee = 'Jason';
&lt;span class="err"&gt;
&lt;/span&gt;    return {
      statusCode: 200,
      body: `You booped ${boopee} on the nose. Boop!`,
    };
  };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we run &lt;code&gt;netlify dev&lt;/code&gt; and call our function, the logs will show us the following output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request from ::1: GET /.netlify/functions/boop-a-friend?boopee=Marisa
[Object: null prototype] { boopee: 'Marisa' }
Response with status 200 in 0 ms.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our &lt;code&gt;boopee&lt;/code&gt; is there!&lt;/p&gt;

&lt;p&gt;Now we can use it by making a few more adjustments to our code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;  exports.handler = async event =&amp;gt; {
&lt;span class="gd"&gt;-   console.log(event.queryStringParameters);
-
-   // TODO get this value from the query string
-   const boopee = 'Jason';
&lt;/span&gt;&lt;span class="gi"&gt;+   const querystring = event.queryStringParameters;
+   const boopee = querystring.boopee || 'a friend';
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    return {
      statusCode: 200,
      body: `You booped ${boopee} on the nose. Boop!`,
    };
  };
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can call our function by visiting &lt;code&gt;http://localhost:9999/.netlify/functions/boop-a-friend?boopee=Marisa&lt;/code&gt; and we'll see that we're booping Marisa, just like we wanted to!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F3fwuiq3q0nohcezdbayn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2F3fwuiq3q0nohcezdbayn.png" alt="Browser showing output with a query parameter: “You booped Marisa on the nose. Boop!”"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we don't add a query string, we fall back to the default text:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fzfj14ydtnpdf4svu085j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fzfj14ydtnpdf4svu085j.png" alt="Browser showing output without a query parameter: “You booped a friend on the nose. Boop!”"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We did it! We can now send, parse, and use query string parameters in our serverless functions!&lt;/p&gt;

&lt;h2&gt;
  
  
  What to do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.learnwithjason.dev/blog/serverless-functions/overview/" rel="noopener noreferrer"&gt;See the full collection of serverless function examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Read the &lt;a href="https://docs.netlify.com/cli/get-started/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment" rel="noopener noreferrer"&gt;Netlify CLI docs on setting up continuous deployment&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>jamuary</category>
      <category>frontend</category>
      <category>serverless</category>
      <category>jamstack</category>
    </item>
    <item>
      <title>Deploy Your First Serverless Function Using JavaScript</title>
      <dc:creator>Jason Lengstorf</dc:creator>
      <pubDate>Wed, 08 Jan 2020 16:38:18 +0000</pubDate>
      <link>https://dev.to/jlengstorf/deploy-your-first-serverless-function-using-javascript-1g4e</link>
      <guid>https://dev.to/jlengstorf/deploy-your-first-serverless-function-using-javascript-1g4e</guid>
      <description>&lt;p&gt;&lt;strong&gt;Serverless functions are a powerful solution for adding additional functionality to JAMstack sites.&lt;/strong&gt; In this post, we'll step through creating and deploying your first serverless function using &lt;a href="https://www.netlify.com/products/functions/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex"&gt;Netlify Functions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write your first serverless function
&lt;/h2&gt;

&lt;p&gt;Our first step is to write the serverless function itself. In an empty folder, create a folder called &lt;code&gt;functions&lt;/code&gt;, and create a new file called &lt;code&gt;my-first-function.js&lt;/code&gt; inside with the following code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&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;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;boop&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;That's all there is to it — you've just written your first serverless function! 🎉 The rest of this article is about getting this function online; we're done coding now.&lt;/p&gt;
&lt;h3&gt;
  
  
  What are the requirements of a serverless function?
&lt;/h3&gt;

&lt;p&gt;There are three required components in a serverless function:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The file needs to export a function named &lt;code&gt;handler&lt;/code&gt; — this is what &lt;code&gt;exports.handler&lt;/code&gt; is doing on line 1 above&lt;/li&gt;
&lt;li&gt;The function needs to return an object with a &lt;code&gt;statusCode&lt;/code&gt; matching a valid HTTP response code&lt;/li&gt;
&lt;li&gt;The response object also needs to include a &lt;code&gt;body&lt;/code&gt; value, which is plain text by default&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Configure your project for deployment to Netlify
&lt;/h2&gt;

&lt;p&gt;With Netlify Functions, we only need two lines of configuration, which we need to save in &lt;code&gt;netlify.toml&lt;/code&gt; at the root of the folder:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[build]&lt;/span&gt;
  &lt;span class="py"&gt;functions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"functions"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;This tells Netlify that our functions live in the &lt;code&gt;functions&lt;/code&gt; folder.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Heads up!&lt;/strong&gt; Check the docs for details on &lt;a href="https://docs.netlify.com/configure-builds/file-based-configuration/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment"&gt;how Netlify config files work&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Create the repo and push to GitHub
&lt;/h3&gt;

&lt;p&gt;At this point, we‘re ready to get this function on the internet!&lt;/p&gt;

&lt;p&gt;Create a new repo on GitHub, then add and push our code to it:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# add your new repo as an origin&lt;/span&gt;
&lt;span class="c"&gt;# IMPORTANT: make sure to use your own username/repo name!&lt;/span&gt;
git remote add origin git@github.com:yourusername/yourreponame.git

&lt;span class="c"&gt;# add all the files&lt;/span&gt;
git add &lt;span class="nt"&gt;-A&lt;/span&gt;

&lt;span class="c"&gt;# commit the files&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s1"&gt;'my first serverless function'&lt;/span&gt;

&lt;span class="c"&gt;# push the changes to GitHub&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;IMPORTANT:&lt;/strong&gt; make sure to use your own username and repo name when you add the origin above!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Create a new Netlify site
&lt;/h3&gt;

&lt;p&gt;You can create your site through the Netlify dashboard or through the CLI. The CLI is really convenient and powerful, so let's use that for this site.&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install the Netlify CLI globally&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--global&lt;/span&gt; netlify-cli

&lt;span class="c"&gt;# log into your Netlify account&lt;/span&gt;
netlify login

&lt;span class="c"&gt;# initialize a new site&lt;/span&gt;
netlify init
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;This command will set up a new Netlify site in your account connected to the GitHub repo we just created.&lt;/p&gt;

&lt;p&gt;It will ask several questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What would you like to do?&lt;/strong&gt; — choose "Create &amp;amp; configure a new site"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Team&lt;/strong&gt; — choose which Netlify team you want to add this site to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site name (optional)&lt;/strong&gt; — choose a name for your site, or press enter to get a randomly-generated name&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your build command&lt;/strong&gt; — press enter to leave this blank; we don't need it for running functions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Directory to deploy&lt;/strong&gt; — hit backspace to remove the suggested value, then press enter to leave it blank&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7iKxDr8a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/ebcpc0ervctefrx88ebx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7iKxDr8a--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/ebcpc0ervctefrx88ebx.png" alt="Screenshot of terminal output from running netlify init with the above settings."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the site has been created, we can grab the URL from the terminal output. In the above screenshot, the generated site name was:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://confident-nightingale-4e5a0b.netlify.com/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;


&lt;p&gt;By default, Netlify functions live at the URL endpoint &lt;code&gt;/.netlify/functions/&amp;lt;function-name&amp;gt;&lt;/code&gt; — this is to minimize the chances that the route will conflict with other routes on your site. (We can &lt;a href="https://docs.netlify.com/routing/redirects/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment"&gt;customize our function URLs with redirects&lt;/a&gt;, if we want to.)&lt;/p&gt;

&lt;p&gt;Our function file is called &lt;code&gt;my-first-function.js&lt;/code&gt;, so it will be accessible on the web at &lt;a href="https://confident-nightingale-4e5a0b.netlify.com/.netlify/functions/my-first-function"&gt;https://confident-nightingale-4e5a0b.netlify.com/.netlify/functions/my-first-function&lt;/a&gt;. Go ahead and click that link — it works!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nzjieAuI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/yk0t7l1iiea2hg863it8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nzjieAuI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev.s3.amazonaws.com/i/yk0t7l1iiea2hg863it8.png" alt="Browser showing the “boop” returned by the serverless function."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's all there is to it! You've successfully deployed your first serverless function to Netlify.&lt;/p&gt;
&lt;h2&gt;
  
  
  What to do next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.learnwithjason.dev/blog/serverless-functions/overview/"&gt;See the full collection of serverless function examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Read the &lt;a href="https://docs.netlify.com/cli/get-started/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment"&gt;Netlify CLI docs on setting up continuous deployment&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Learn how to &lt;a href="https://docs.netlify.com/routing/redirects/?utm_source=learnwithjason&amp;amp;utm_medium=first-serverless-function-jl&amp;amp;utm_campaign=devex#continuous-deployment"&gt;use redirects in Netlify&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  More posts like this
&lt;/h2&gt;


&lt;div class="ltag__tag ltag__tag__id__41991"&gt;
  
    .ltag__tag__id__41991 .follow-action-button{
      background-color:  !important;
      color:  !important;
      border-color:  !important;
    }
  
    &lt;div class="ltag__tag__content"&gt;
      &lt;h2&gt;#&lt;a href="/t/jamuary" class="ltag__tag__link"&gt;jamuary&lt;/a&gt; 
&lt;/h2&gt;
      &lt;div class="ltag__tag__summary"&gt;
        
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>jamuary</category>
      <category>frontend</category>
      <category>serverless</category>
      <category>jamstack</category>
    </item>
  </channel>
</rss>
