<?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: Ivan</title>
    <description>The latest articles on DEV Community by Ivan (@ivandotcodes).</description>
    <link>https://dev.to/ivandotcodes</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F896210%2F11fbdbf9-91a7-4d24-8fa7-2eeade88b63f.jpeg</url>
      <title>DEV Community: Ivan</title>
      <link>https://dev.to/ivandotcodes</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ivandotcodes"/>
    <language>en</language>
    <item>
      <title>The best SaaS video tools in 2026</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:18:17 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/the-best-saas-video-tools-in-2026-1bfn</link>
      <guid>https://dev.to/ivandotcodes/the-best-saas-video-tools-in-2026-1bfn</guid>
      <description>&lt;p&gt;SaaS teams tend to need two kinds of video. There is the short promo, the 15 to 60 second piece that goes on the homepage, in a launch post or in a paid social campaign, and there is the explainer, which is longer and walks someone through what the product does and why it matters. Some tools are good at one and poor at the other, so I have tried to be clear about which is which.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Teasr
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuzr2s9f4flknw82ptjh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuzr2s9f4flknw82ptjh.gif" alt="Teasr reading a website and turning its headline, product photo, fonts and colours into a video storyboard" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Teasr makes short promo videos from a website. You paste your URL, it reads your colours, fonts, copy and product images, turns them into a 15 to 60 second motion video and writes an original piece of music for it. Most videos are ready in about 10 minutes, at 1080p and 60 fps, and you ask for changes in plain language.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohqpckvv2ghkmbqm69cu.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohqpckvv2ghkmbqm69cu.gif" alt="A few seconds of a Teasr video made from the website of TapNoise, a keyboard sound app" width="640" height="360"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A few seconds of a video Teasr made from the website of TapNoise, a keyboard sound app.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; SaaS teams that want an on-brand promo for the homepage, a launch or a social campaign without briefing an agency or opening a motion design tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; Teasr is built for promos, not explainers. It does not record your screen, has no voiceover or avatars and is not a general video editor, so it cannot walk someone through a feature step by step. It also depends on your website, so a site with few visuals gives it less to work with.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Synthesia
&lt;/h2&gt;

&lt;p&gt;Synthesia generates videos with AI presenters. You write a script, pick an avatar and a voice, and it produces a talking-head video in many languages, which is why it is popular for training, onboarding and internal explainers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; explainer and onboarding videos that need a presenter, especially when you have to produce them in several languages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; an AI presenter in front of a slide is a good fit for training content but tends to feel corporate for a homepage promo, and many viewers can tell it is generated.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. HeyGen
&lt;/h2&gt;

&lt;p&gt;HeyGen is another AI avatar video platform, with stock and custom avatars, voice cloning and translation, aimed at marketing and sales teams as much as training.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; personalised sales videos, translated versions of existing videos, and explainers with a presenter where you want a custom avatar of someone on your team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; like Synthesia, the output centres on a person talking, which is the wrong shape for a short brand promo, and usage is metered in credits, so heavy use needs watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Descript
&lt;/h2&gt;

&lt;p&gt;Descript is a video and podcast editor where you edit by editing the transcript, with AI tools for removing filler words, cleaning up audio, recording your screen and generating clips.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; founder-led explainer videos and screen-recorded walkthroughs where someone on the team is talking and you want to edit quickly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; it makes recorded footage better, but it does not design anything for you, so a polished, animated promo is still on you.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Canva
&lt;/h2&gt;

&lt;p&gt;Canva is the design tool most marketing teams already use, and its video editor, templates and AI generation tools mean you can make a promo next to the rest of your social assets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that already use Canva for brand assets and want to make simple promos and social clips from templates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; template-based videos look like templates unless someone with design skill reworks them, and getting animation to feel tied to your product takes time.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Animoto
&lt;/h2&gt;

&lt;p&gt;Animoto is a drag-and-drop video maker built around templates, with screen and webcam recording, stock media and a licensed music library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; small marketing teams that want to assemble promo and social videos from their own clips and photos without learning an editor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the templates lean towards general business and social content, so a software product with UI screenshots can feel squeezed into a format built for photos.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Renderforest
&lt;/h2&gt;

&lt;p&gt;Renderforest combines template-based video making with AI video, image and voice generation on a credit system, alongside logo and website tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that want a broad set of templates and AI generation models in one subscription and are happy to work within credits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the cheapest plan exports templates at 720p, and with so many models and credit types it takes some time to understand what you are paying for.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. An agency or freelance motion designer
&lt;/h2&gt;

&lt;p&gt;For a flagship explainer or a brand film, a human team with a scriptwriter, illustrator, animator and voice actor is still the high end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; funded SaaS companies that need a custom explainer which will sit on the homepage and in sales decks for a year or more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; production takes weeks, the cost is far higher than any tool here, and when your product changes the video goes out of date with no quick way to update it.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;Decide first whether you need a promo or an explainer. For a short promo that looks like your brand, Teasr gets you there fastest when your website is already in good shape, and Canva or Animoto work if you want to assemble one yourself from templates. For an explainer with a person talking, Descript is the best choice when that person is you or a teammate, and Synthesia or HeyGen are worth a look when you would rather use an AI presenter or need many languages. Renderforest sits in between, with lots of templates and generation models in one place. An agency makes sense when the video is a long-term centrepiece and the budget allows it. Many SaaS teams end up with one of each, a short promo for the top of the funnel and a longer explainer for people who are already interested.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Starting price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Teasr&lt;/td&gt;
&lt;td&gt;On-brand promo from your website in about 10 minutes&lt;/td&gt;
&lt;td&gt;€19.50 per 15s video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Synthesia&lt;/td&gt;
&lt;td&gt;AI presenter explainers and training in many languages&lt;/td&gt;
&lt;td&gt;Free, Starter $18/month yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HeyGen&lt;/td&gt;
&lt;td&gt;Avatar videos, personalised sales clips, translation&lt;/td&gt;
&lt;td&gt;Free, Creator $24/month yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Descript&lt;/td&gt;
&lt;td&gt;Editing founder-led and screen-recorded explainers&lt;/td&gt;
&lt;td&gt;Free, Hobbyist $16/month yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Canva&lt;/td&gt;
&lt;td&gt;Template promos next to your other brand assets&lt;/td&gt;
&lt;td&gt;Free, Pro $144/year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Animoto&lt;/td&gt;
&lt;td&gt;Drag-and-drop promos from your own clips&lt;/td&gt;
&lt;td&gt;Free, Basic €7/month yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Renderforest&lt;/td&gt;
&lt;td&gt;Templates plus AI generation on credits&lt;/td&gt;
&lt;td&gt;Lite €8/month yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Agency or freelancer&lt;/td&gt;
&lt;td&gt;Custom flagship explainers&lt;/td&gt;
&lt;td&gt;Varies by quote&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>saas</category>
      <category>marketing</category>
      <category>startup</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The best demo video tools in 2026</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:08:02 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/the-best-demo-video-tools-in-2026-18cb</link>
      <guid>https://dev.to/ivandotcodes/the-best-demo-video-tools-in-2026-18cb</guid>
      <description>&lt;p&gt;"Demo video" covers a few different things. There is the walkthrough, where you show someone how to do a task step by step. There is the interactive demo, a clickable tour of your product that a visitor can explore on your website without signing up. And there is the short overview, a quick, polished piece that shows what the product is and how it feels before anyone commits to a longer demo. The first two are where screen recorders and interactive demo platforms shine, and the third is where Teasr fits.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Teasr
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuzr2s9f4flknw82ptjh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuzr2s9f4flknw82ptjh.gif" alt="Teasr reading a website and turning its headline, product photo, fonts and colours into a video storyboard" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Teasr turns a website URL into a 15 to 60 second motion video. It pulls your colours, fonts, copy and product images from the page, animates them into a short piece in your brand and writes original music for it, and the video is usually ready in about 10 minutes, at 1080p and 60 fps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fev2qdfhdgku6edpxevv5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fev2qdfhdgku6edpxevv5.gif" alt="A few seconds of a Teasr video made from the website of Stoll Kaffee, a coffee roaster in Zürich" width="640" height="360"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A few seconds of a video Teasr made from the website of Stoll Kaffee, a coffee roaster in Zürich.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; a short, polished overview at the top of a page or a social post, the piece that makes someone want to see the full demo.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; Teasr is not a demo tool in the walkthrough sense. It does not record your screen, there is no voiceover or avatar, it is not an editor, and it works from your website, so it cannot show someone how to complete a task in your app. If that is what you need, use one of the tools below.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Screen Studio
&lt;/h2&gt;

&lt;p&gt;Screen Studio is a Mac screen recorder that turns raw recordings into clean product videos, with automatic zoom on clicks, smoothed cursor movement, captions, device frames and layouts for your camera.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; recorded product walkthroughs that need to look polished, such as feature announcements, onboarding videos and tutorials.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; it only runs on macOS, and while it makes a recording look good, the pacing and the story still depend on how well you plan and record the take.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Loom
&lt;/h2&gt;

&lt;p&gt;Loom is the quick way to record your screen and camera and share a link, with transcripts, comments and viewer analytics built in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; fast, informal demos for a specific person, like a prospect, a customer with a question or a teammate, where speed matters more than polish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; Loom videos look like what they are, a person talking over their screen, which is perfect for one-to-one communication but less suited to a demo on your homepage.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Arcade
&lt;/h2&gt;

&lt;p&gt;Arcade captures your product as you click through it and turns that into an interactive demo with hotspots and callouts, and it can also produce video versions with AI voiceover.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; clickable demos embedded on a landing page, in docs or in a launch post, where you want visitors to explore at their own pace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the free plan's single published demo and video is too little for most teams, so in practice you are looking at the paid plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Supademo
&lt;/h2&gt;

&lt;p&gt;Supademo is an interactive demo platform that lets you build guided demos from screenshots or recordings, with AI for personalising text, branching, analytics and, on higher plans, HTML-based demos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; teams that want to start with interactive demos on a free plan and grow into sales and customer success use cases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the more advanced features, like HTML demos and sandboxes, sit on the much more expensive Business and Growth plans.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Storylane
&lt;/h2&gt;

&lt;p&gt;Storylane builds guided interactive tours from screenshots, videos or captured HTML, with lead capture, analytics and AI voiceovers, and it targets both marketing and sales teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; B2B SaaS companies that want interactive demos on the website and in sales conversations, with CRM integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the jump from Starter to Growth is large, and HTML demos, A/B testing and personalisation tokens all live on Growth or above.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Navattic
&lt;/h2&gt;

&lt;p&gt;Navattic is an interactive demo platform aimed at product marketing and sales teams, with HTML capture, AI copilot features, AI avatars on paid plans and deep CRM and sales tool integrations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; larger SaaS teams that want interactive demos tied into their marketing and sales stack, with account identification and pipeline reporting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; with no public prices on the paid plans, it is hard to budget for without a sales conversation, and it is more platform than a small team needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Descript
&lt;/h2&gt;

&lt;p&gt;Descript records your screen, webcam and microphone and lets you edit the result by editing the transcript, with tools for removing filler words and cleaning up audio.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; longer narrated demos and tutorials where someone is talking throughout and you want to cut mistakes as if you were editing a document.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; it is an editor first, so screen recordings do not get the automatic zoom and cursor polish of a dedicated tool like Screen Studio without extra work.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;Start with what the viewer should be able to do afterwards. If they need to learn how to do something in your product, record it, with Screen Studio for polish, Loom for speed or Descript for narrated, editable tutorials. If they should explore the product themselves before talking to you, an interactive demo is the better format, with Arcade and Supademo being the easiest to start with and Storylane and Navattic built for bigger sales teams. If they just need to understand what the product is and feel something about it in under a minute, that is where a short overview from Teasr fits, and it works well at the top of a page with a walkthrough or interactive demo further down. Most teams need two of these, and very few need all of them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Starting price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Teasr&lt;/td&gt;
&lt;td&gt;Short, polished product overview from your website&lt;/td&gt;
&lt;td&gt;€19.50 per 15s video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen Studio&lt;/td&gt;
&lt;td&gt;Polished recorded walkthroughs (Mac)&lt;/td&gt;
&lt;td&gt;$9/month billed yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Loom&lt;/td&gt;
&lt;td&gt;Quick one-to-one screen and camera demos&lt;/td&gt;
&lt;td&gt;Free, Business $18/user/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arcade&lt;/td&gt;
&lt;td&gt;Embeddable clickable demos&lt;/td&gt;
&lt;td&gt;Free, Growth $42.50/seat/month yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supademo&lt;/td&gt;
&lt;td&gt;Interactive demos with a usable free plan&lt;/td&gt;
&lt;td&gt;Free, Scale $38/month yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storylane&lt;/td&gt;
&lt;td&gt;Interactive demos for B2B marketing and sales&lt;/td&gt;
&lt;td&gt;Free, Starter $40/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Navattic&lt;/td&gt;
&lt;td&gt;Interactive demos tied into the sales stack&lt;/td&gt;
&lt;td&gt;Free, paid plans on request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Descript&lt;/td&gt;
&lt;td&gt;Narrated tutorials edited by transcript&lt;/td&gt;
&lt;td&gt;Free, Hobbyist $16/month yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>saas</category>
      <category>productivity</category>
      <category>startup</category>
      <category>marketing</category>
    </item>
    <item>
      <title>The best tools for making launch videos in 2026</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Mon, 12 Oct 2026 17:03:29 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/the-best-tools-for-making-launch-videos-in-2026-473o</link>
      <guid>https://dev.to/ivandotcodes/the-best-tools-for-making-launch-videos-in-2026-473o</guid>
      <description>&lt;p&gt;A launch video has a narrow job. It sits at the top of a Product Hunt page, gets posted on X and LinkedIn on launch day, and lives in the hero of your landing page afterwards. It should be short, it should look like your product and your brand, and it should make someone curious enough to click. That is a different job from a tutorial or a sales demo, so the tools below are chosen for that job.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Teasr
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuzr2s9f4flknw82ptjh.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbuzr2s9f4flknw82ptjh.gif" alt="Teasr reading a website and turning its headline, product photo, fonts and colours into a video storyboard" width="600" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Teasr turns a website URL into a 15 to 60 second motion video. You paste your landing page, and it pulls your colours, fonts, headlines and product images, animates them into a short promo and writes original music for it. A video is usually ready in about 10 minutes, at 1080p and 60 fps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Friy145v05uku8sxey8ea.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Friy145v05uku8sxey8ea.gif" alt="A few seconds of a Teasr video made from the website of toni keramik, a small ceramics studio" width="640" height="360"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;A few seconds of a video Teasr made from the website of toni keramik, a small ceramics studio.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; founders who already have a decent landing page and want a polished, on-brand launch teaser without learning motion design or hiring someone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; Teasr works from your website, so if the site is rough or mostly text, the video will be too. It does not record your screen, there are no avatars or voiceover, and it is not a general video editor, so if you want to show a real click-through of your product you will need one of the tools below as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Screen Studio
&lt;/h2&gt;

&lt;p&gt;Screen Studio is a Mac screen recorder that adds automatic zooms, smooth cursor movement, device frames and captions to whatever you record, so a plain screen capture comes out looking like an edited product video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; makers who want the launch video to show the product in use, and who are comfortable recording and timing a short walkthrough themselves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; it is macOS only, and the result is only as good as the take you record, so you still have to plan the flow and record it cleanly.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Jitter
&lt;/h2&gt;

&lt;p&gt;Jitter is a browser-based motion design tool, closer to a lighter Figma for animation than to a video editor. You build scenes from your own designs and animate them on a timeline, with templates to start from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; designers, or founders who think like one, who want full control over every frame of a short animated launch clip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; you are doing the motion design yourself, which takes time and taste, and Jitter does not give you music, so you still need to source a track.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Rotato
&lt;/h2&gt;

&lt;p&gt;Rotato is a Mac app for 3D device mockups. You drop in a screenshot or screen recording of your app, place it on a phone, laptop or desktop, and animate the camera around it to export a slick product shot or video.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; mobile app launches and App Store or Product Hunt galleries where a rotating phone with your UI on it is exactly what you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; it is macOS only and it is a mockup tool, so it frames your UI beautifully but does not tell a story or add music; most people cut its exports into a longer video elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Arcade
&lt;/h2&gt;

&lt;p&gt;Arcade started as a tool for clickable, interactive product demos and now also generates videos with AI voiceover from those captures. For a launch, that means you can embed an interactive walkthrough on your landing page and export a video version for social.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; SaaS launches where you want visitors to try a guided version of the product right on the landing page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; the free plan is too tight for a real launch with more than one asset, and an interactive demo is a different thing from a punchy 20 second teaser for a feed.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. CapCut
&lt;/h2&gt;

&lt;p&gt;CapCut is a free video editor from ByteDance, available on desktop, mobile and the web, with templates, captions, effects and a growing set of AI generation tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; founders on a zero budget who are happy to edit their own launch clip, especially vertical clips for X, TikTok or Reels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; it is an editor, so you bring the footage, the structure and the brand consistency yourself, and its templates are built for social content rather than for software products.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. A freelancer or motion design studio
&lt;/h2&gt;

&lt;p&gt;The traditional route is still a good one. A good motion designer will write a script with you, design custom scenes and deliver something none of the tools above will match when it comes to originality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best for:&lt;/strong&gt; funded launches where the video is a central asset that will run in ads and on the homepage for months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation:&lt;/strong&gt; it takes weeks rather than minutes, costs a multiple of everything else on this list, and each round of changes goes through another person's calendar, which is hard when your launch date is fixed.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose
&lt;/h2&gt;

&lt;p&gt;Start from what you want people to see. If the point is the brand and the promise, a short motion teaser works best, and Teasr is the quickest way to get one if your landing page already looks good, while Jitter gives you full control if you have the time and design skill. If the point is the product working, record it with Screen Studio, or put a phone or laptop around it with Rotato. If you want visitors to try the product before signing up, an interactive demo from Arcade belongs on your landing page next to whatever video you post on launch day. CapCut is the right answer when the budget is zero and you have an afternoon, and a freelancer or studio is the right answer when the video matters for months and you have both time and money. Many launches end up using two of these together, a short teaser for the feed and a walkthrough for the page.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;Starting price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Teasr&lt;/td&gt;
&lt;td&gt;On-brand launch teaser from your website, about 10 minutes&lt;/td&gt;
&lt;td&gt;€19.50 per 15s video&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Screen Studio&lt;/td&gt;
&lt;td&gt;Polished screen recordings of the product in use (Mac)&lt;/td&gt;
&lt;td&gt;$9/month billed yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jitter&lt;/td&gt;
&lt;td&gt;Hands-on motion design in the browser&lt;/td&gt;
&lt;td&gt;Free, Pro $10/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotato&lt;/td&gt;
&lt;td&gt;3D device mockup videos for apps (Mac)&lt;/td&gt;
&lt;td&gt;€79 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Arcade&lt;/td&gt;
&lt;td&gt;Interactive demo on the landing page&lt;/td&gt;
&lt;td&gt;Free, Growth $42.50/seat/month yearly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CapCut&lt;/td&gt;
&lt;td&gt;Doing it yourself on a zero budget&lt;/td&gt;
&lt;td&gt;Free, see pricing page for paid&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freelancer or studio&lt;/td&gt;
&lt;td&gt;Custom, high-stakes launch films&lt;/td&gt;
&lt;td&gt;Varies by quote&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>startup</category>
      <category>marketing</category>
      <category>saas</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to Get Your Brand Recommended by ChatGPT</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Fri, 23 Jan 2026 23:35:47 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/how-to-get-your-brand-recommended-by-chatgpt-iko</link>
      <guid>https://dev.to/ivandotcodes/how-to-get-your-brand-recommended-by-chatgpt-iko</guid>
      <description>&lt;p&gt;When someone asks ChatGPT "what's the best CRM for small businesses" or "can you recommend a project management tool," the AI provides direct suggestions. If your brand appears in those recommendations, you gain a significant advantage in the consideration process. If you are absent, potential customers may never discover you.&lt;/p&gt;

&lt;p&gt;This guide explains how ChatGPT forms recommendations and what you can do to increase your chances of being recommended.&lt;/p&gt;

&lt;h2&gt;
  
  
  How ChatGPT Decides What to Recommend
&lt;/h2&gt;

&lt;p&gt;ChatGPT does not browse the internet in real-time for most queries. Instead, it draws from patterns learned during training on large amounts of text data. When recommending products or services, ChatGPT references what it learned about brands, their reputations, their features, and how they are discussed across the web.&lt;/p&gt;

&lt;p&gt;Several factors influence whether ChatGPT recommends your brand:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content volume and quality&lt;/strong&gt;: Brands with substantial, high-quality content explaining what they do and why they are valuable are more likely to be well-represented in training data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Third-party validation&lt;/strong&gt;: Reviews, mentions in publications, expert endorsements, and customer testimonials all contribute to how ChatGPT perceives your brand's credibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clear category association&lt;/strong&gt;: ChatGPT needs to understand what category your brand belongs to and what problems it solves. Ambiguous positioning leads to missed recommendations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reputation signals&lt;/strong&gt;: The overall sentiment of how your brand is discussed online influences whether ChatGPT views you as a recommendable option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Audit Your Current Visibility
&lt;/h2&gt;

&lt;p&gt;Before optimizing, understand where you stand. Ask ChatGPT the questions your potential customers might ask about your product category.&lt;/p&gt;

&lt;p&gt;Test queries like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What is the best [your category]?"&lt;/li&gt;
&lt;li&gt;"Can you recommend a [your product type]?"&lt;/li&gt;
&lt;li&gt;"What are the alternatives to [your competitor]?"&lt;/li&gt;
&lt;li&gt;"What [product type] should I use for [use case]?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Document whether your brand appears, where you rank in the recommendation list, how you are described, and which competitors appear instead.&lt;/p&gt;

&lt;p&gt;This baseline helps you prioritize efforts and measure progress. For a detailed framework, see our &lt;a href="https://noserp.com/blog/audit-ai-recommendation-presence" rel="noopener noreferrer"&gt;complete AI audit guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create Comprehensive Content
&lt;/h2&gt;

&lt;p&gt;ChatGPT learns from content available on the web. If your brand has limited content explaining what you offer, why it matters, and who it helps, ChatGPT has little to reference when forming recommendations.&lt;/p&gt;

&lt;p&gt;Develop content that clearly establishes your brand:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Product and service pages&lt;/strong&gt; that explain exactly what you offer, not just feature lists but the problems you solve and outcomes you enable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparison content&lt;/strong&gt; that honestly positions your offering relative to alternatives, helping ChatGPT understand where you fit in the market.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use case content&lt;/strong&gt; addressing specific scenarios where your product excels, making it easier for ChatGPT to match you with relevant queries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FAQ content&lt;/strong&gt; answering common questions about your category and your specific offering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Thought leadership&lt;/strong&gt; demonstrating your expertise in your space through guides, analysis, and educational content.&lt;/p&gt;

&lt;p&gt;The goal is ensuring that when ChatGPT processes information about your category, your brand is well-represented with clear, accurate, and comprehensive information.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Build Third-Party Validation
&lt;/h2&gt;

&lt;p&gt;ChatGPT weighs third-party sources heavily. Self-promotional content on your own website matters, but external validation carries more weight.&lt;/p&gt;

&lt;p&gt;Focus on building:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Customer reviews&lt;/strong&gt; on relevant platforms. Encourage satisfied customers to leave detailed reviews explaining their experience and the value they received.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Media coverage&lt;/strong&gt; in publications your industry respects. Seek opportunities for features, interviews, or expert commentary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expert endorsements&lt;/strong&gt; from recognized authorities in your space. Analysts, industry experts, and respected practitioners can provide valuable validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Case studies and testimonials&lt;/strong&gt; that document real customer success, preferably with specific metrics and outcomes.&lt;/p&gt;

&lt;p&gt;When ChatGPT encounters consistent positive signals about your brand from multiple independent sources, it increases confidence in recommending you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Strengthen Category Association
&lt;/h2&gt;

&lt;p&gt;ChatGPT needs to clearly understand what category your brand belongs to and what specific problems you solve. Weak or ambiguous positioning leads to missed recommendations.&lt;/p&gt;

&lt;p&gt;Ensure your brand is clearly associated with:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your primary category&lt;/strong&gt;: If you are a project management tool, that association should be unmistakable across your content and mentions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Specific problems you solve&lt;/strong&gt;: Beyond category, connect your brand to the particular challenges you address.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your target audience&lt;/strong&gt;: Who is your product for? Enterprise teams, small businesses, specific industries?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key differentiators&lt;/strong&gt;: What makes you distinct from alternatives in your category?&lt;/p&gt;

&lt;p&gt;Consistent messaging across your website, third-party profiles, and external mentions reinforces these associations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Optimize for Related Queries
&lt;/h2&gt;

&lt;p&gt;ChatGPT responds to many different phrasings of similar questions. Consider the various ways potential customers might ask about solutions in your category.&lt;/p&gt;

&lt;p&gt;Create content addressing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direct category queries&lt;/strong&gt;: "best [category]" type searches&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Problem-based queries&lt;/strong&gt;: "how do I solve [problem]" where your product is the answer&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Comparison queries&lt;/strong&gt;: "alternative to [competitor]" or "[product A] vs [product B]"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use case queries&lt;/strong&gt;: "[category] for [specific situation]"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Feature queries&lt;/strong&gt;: "[category] with [specific capability]"&lt;/p&gt;

&lt;p&gt;Content addressing these different query types increases the chances that ChatGPT connects your brand with relevant questions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Maintain Accuracy and Consistency
&lt;/h2&gt;

&lt;p&gt;ChatGPT can become confused when it encounters contradictory information about your brand. Ensure consistency across all platforms:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consistent naming&lt;/strong&gt;: Use the same brand name everywhere, avoiding variations that might not be connected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accurate information&lt;/strong&gt;: Keep product information, pricing, and claims current and accurate across all sources.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unified positioning&lt;/strong&gt;: Communicate the same core value proposition across channels.&lt;/p&gt;

&lt;p&gt;Inconsistencies in your information can reduce ChatGPT's confidence in recommending you or lead to inaccurate descriptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Monitor and Adjust
&lt;/h2&gt;

&lt;p&gt;AI recommendations are not static. ChatGPT's responses can shift as models are updated, and your competitive landscape continues evolving.&lt;/p&gt;

&lt;p&gt;Establish ongoing monitoring:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Regular testing&lt;/strong&gt;: Query ChatGPT monthly with your key questions and document changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Competitor tracking&lt;/strong&gt;: Note when competitors appear or disappear from recommendations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Description accuracy&lt;/strong&gt;: Monitor how ChatGPT describes your brand and correct inaccuracies through content updates.&lt;/p&gt;

&lt;p&gt;Continuous attention allows you to identify and address issues before they become significant problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  What About ChatGPT's Browsing Features?
&lt;/h2&gt;

&lt;p&gt;Some ChatGPT users have access to browsing capabilities that allow real-time web searches. For these queries, traditional SEO factors become more relevant since ChatGPT actively searches for current information.&lt;/p&gt;

&lt;p&gt;Strong search visibility complements AI recommendation optimization. Brands that perform well in both areas capture opportunities regardless of how users interact with ChatGPT.&lt;/p&gt;

&lt;p&gt;For more on how traditional SEO and AI optimization work together, see our &lt;a href="https://noserp.com/blog/geo-vs-seo-difference" rel="noopener noreferrer"&gt;GEO vs SEO guide&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Mistakes to Avoid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Expecting immediate results&lt;/strong&gt;: ChatGPT's knowledge is based on training data, not real-time information. Changes you make today may take time to influence recommendations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Focusing only on your website&lt;/strong&gt;: Third-party validation often matters more than your own content. Balance investment between owned content and external presence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ignoring competitors&lt;/strong&gt;: Understanding what competitors who get recommended are doing differently provides valuable insights for your own strategy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generic content&lt;/strong&gt;: Vague marketing language does not help ChatGPT understand what makes you valuable. Be specific about capabilities, outcomes, and differentiators.&lt;/p&gt;

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

&lt;p&gt;Begin with an audit of your current ChatGPT visibility. Understanding your baseline tells you whether you need foundational work or refinement of existing presence.&lt;/p&gt;

&lt;p&gt;If you are absent from recommendations entirely, prioritize comprehensive content development and third-party validation. If you appear but are poorly positioned, focus on strengthening specific weak areas.&lt;/p&gt;

&lt;p&gt;The brands establishing ChatGPT recommendation presence now are building advantages that become harder for competitors to overcome as AI assistants become more central to how people discover products and services.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Want to understand how your brand currently performs in AI recommendations? &lt;a href="https://noserp.com" rel="noopener noreferrer"&gt;Get a free visibility report&lt;/a&gt; to see where you stand and what to prioritize.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Git Isn't Enough for AI Coding: What I Use Instead</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Fri, 23 Jan 2026 22:59:50 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/git-isnt-enough-for-ai-coding-what-i-use-instead-2gp5</link>
      <guid>https://dev.to/ivandotcodes/git-isnt-enough-for-ai-coding-what-i-use-instead-2gp5</guid>
      <description>&lt;p&gt;I've been using Git for over a decade. It's one of the best tools ever made for software development. But after six months of heavy AI-assisted coding, I've had to admit something uncomfortable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git's workflow doesn't match how AI coding actually works.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This isn't a criticism of Git. It's an observation about how AI assistants have changed the development loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Git was designed for deliberate, discrete changes. You work on something, stage it, write a meaningful commit message, and move on. The rhythm is: work → commit → work → commit.&lt;/p&gt;

&lt;p&gt;AI coding breaks this rhythm completely.&lt;/p&gt;

&lt;p&gt;When I'm iterating with Cursor or Claude Code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I try three different approaches in ten minutes&lt;/li&gt;
&lt;li&gt;The AI modifies files I didn't explicitly ask about&lt;/li&gt;
&lt;li&gt;Changes happen faster than I can meaningfully review them&lt;/li&gt;
&lt;li&gt;I'm focused on guiding the AI, not thinking about version control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, I end up with long gaps between commits. And those gaps are exactly when AI does something unexpected—deletes files it thinks are "unused," breaks imports, or refactors in a direction I didn't want.&lt;/p&gt;

&lt;p&gt;Git protects what I commit. It can't protect what I don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Tried First
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;More discipline&lt;/strong&gt;: "Just commit before every AI operation." This works in theory. In practice, when I'm in flow and iterating quickly, I skip it. The one time I forget is the time Cursor deletes my middleware folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cursor Checkpoints&lt;/strong&gt;: These are useful but limited. They only cover Composer interactions, don't persist across sessions, and revert everything from a prompt (including manual edits I made after).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VS Code Local History&lt;/strong&gt;: File-by-file recovery. Tedious when AI changed 20 files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git stash&lt;/strong&gt;: Manual, requires discipline, only one stash at a time.&lt;/p&gt;

&lt;p&gt;None of these provided reliable, continuous protection.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Use Now
&lt;/h2&gt;

&lt;p&gt;I found a tool called &lt;a href="https://www.getmrq.com" rel="noopener noreferrer"&gt;mrq&lt;/a&gt; that takes a different approach. Instead of requiring commits, it captures every file change automatically in the background.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm install -g mrq-cli
mrq watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. Now every change is recorded—from AI, from manual edits, from anything.&lt;/p&gt;

&lt;p&gt;When something breaks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mrq history          # See recent
snapshotsmrq restore abc123   # Go back to any point
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm back to exactly where I was, even if I never committed. The &lt;a href="https://www.getmrq.com/docs" rel="noopener noreferrer"&gt;mrq docs&lt;/a&gt; explain the full workflow, but honestly the basics are just those two commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Use Both Together
&lt;/h2&gt;

&lt;p&gt;I haven't replaced Git. I use both for different purposes:&lt;br&gt;
Git: Meaningful commits, collaboration, deployment, PR reviews&lt;br&gt;
mrq: Continuous protection during AI iteration&lt;/p&gt;

&lt;p&gt;My workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start session: &lt;code&gt;mrq watch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Work with AI freely, iterate fast&lt;/li&gt;
&lt;li&gt;Hit a milestone: &lt;code&gt;git commit -m "Feature complete"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Something broke? &lt;code&gt;mrq restore&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Git gives me clean history for collaboration. mrq gives me protection during the messy iteration phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  When You Need This
&lt;/h2&gt;

&lt;p&gt;If you're doing light AI usage—occasional Copilot suggestions, simple completions—Git alone is probably fine.&lt;/p&gt;

&lt;p&gt;If you're doing heavy AI coding—Cursor Composer, Claude Code multi-file edits, agent mode—you need continuous protection. The speed and unpredictability of AI changes creates gaps that Git's commit model doesn't cover.&lt;/p&gt;

&lt;p&gt;There's a good breakdown of &lt;a href="https://www.getmrq.com/blog/git-not-built-for-ai" rel="noopener noreferrer"&gt;why git doesn't fit AI workflows&lt;/a&gt; that explains this in more detail.&lt;/p&gt;

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

&lt;p&gt;Git remains essential. It's still the backbone of collaboration, deployment, and project history.&lt;/p&gt;

&lt;p&gt;But AI coding has exposed a gap between commits where you're unprotected. For me, the solution was adding &lt;a href="https://www.getmrq.com" rel="noopener noreferrer"&gt;automatic snapshots&lt;/a&gt; to my workflow—not replacing Git, but complementing it.&lt;/p&gt;

&lt;p&gt;The developers I know who work confidently with AI have all set up some form of continuous protection. The specific tool matters less than having something that doesn't require discipline to remember.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What do you use for AI coding protection? I'm curious if there are other approaches I haven't tried.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>git</category>
      <category>programming</category>
      <category>vibecoding</category>
    </item>
    <item>
      <title>The Best Way to Use Teenage Engineering Manuals</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Wed, 21 Jan 2026 17:45:19 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/he-best-way-to-use-teenage-engineering-manuals-4n96</link>
      <guid>https://dev.to/ivandotcodes/he-best-way-to-use-teenage-engineering-manuals-4n96</guid>
      <description>&lt;p&gt;If you own a Teenage Engineering device, you've probably tried to use the official PDF manual. The &lt;a href="https://www.teenagemanual.com/ep-133/ask" rel="noopener noreferrer"&gt;EP-133 manual&lt;/a&gt; is 144 pages. The &lt;a href="https://www.teenagemanual.com/op-1/ask" rel="noopener noreferrer"&gt;OP-1 manual&lt;/a&gt; is 100+ pages. The &lt;a href="https://www.teenagemanual.com/op-xy/ask" rel="noopener noreferrer"&gt;OP-XY manual&lt;/a&gt; is even longer. These are dense technical documents that assume you'll read them cover-to-cover, but in reality you just need quick answers to specific questions.&lt;/p&gt;

&lt;p&gt;There's a better way: &lt;a href="https://www.teenagemanual.com" rel="noopener noreferrer"&gt;Teenage Manual&lt;/a&gt; - a searchable, conversational interface for all Teenage Engineering device manuals. Instead of scrolling through PDFs, you can ask questions and get direct answers from the official documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.teenagemanual.com" rel="noopener noreferrer"&gt;Teenage Manual&lt;/a&gt; is the best way to access Teenage Engineering manuals. It supports 9 devices with full manual access:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/ep-133/ask" rel="noopener noreferrer"&gt;EP-133 (KO II)&lt;/a&gt; - The sampler&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/op-1/ask" rel="noopener noreferrer"&gt;OP-1&lt;/a&gt; - The original portable synth&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/op-1f/ask" rel="noopener noreferrer"&gt;OP-1 Field&lt;/a&gt; - The upgraded version&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/op-xy/ask" rel="noopener noreferrer"&gt;OP-XY&lt;/a&gt; - The latest flagship&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/ep-1320/ask" rel="noopener noreferrer"&gt;EP-1320&lt;/a&gt; - The medieval-themed sampler&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/ep-40/ask" rel="noopener noreferrer"&gt;EP-40&lt;/a&gt; - The riddim machine&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/tx-6/ask" rel="noopener noreferrer"&gt;TX-6&lt;/a&gt; - The portable mixer&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/tp-7/ask" rel="noopener noreferrer"&gt;TP-7&lt;/a&gt; - The field recorder&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/cm-15/ask" rel="noopener noreferrer"&gt;CM-15&lt;/a&gt; - The modular controller&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each device has its own page with two ways to access the manual: ask questions in a chat interface that knows the manual inside and out, or browse the manual in a structured view organized by category.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The AI chat interface is available on each device's ask page - for example, &lt;a href="https://www.teenagemanual.com/ep-133/ask" rel="noopener noreferrer"&gt;EP-133 ask&lt;/a&gt;, &lt;a href="https://www.teenagemanual.com/op-1/ask" rel="noopener noreferrer"&gt;OP-1 ask&lt;/a&gt;, &lt;a href="https://www.teenagemanual.com/op-1f/ask" rel="noopener noreferrer"&gt;OP-1 Field ask&lt;/a&gt;, &lt;a href="https://www.teenagemanual.com/op-xy/ask" rel="noopener noreferrer"&gt;OP-XY ask&lt;/a&gt;, &lt;a href="https://www.teenagemanual.com/ep-1320/ask" rel="noopener noreferrer"&gt;EP-1320 ask&lt;/a&gt;, or &lt;a href="https://www.teenagemanual.com/ep-40/ask" rel="noopener noreferrer"&gt;EP-40 ask&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The AI uses the full OCR'd manual as context, so it's not making things up - it's giving you answers directly from the official documentation. Ask "how do I copy a pattern?" and it tells you exactly how, with the right button combinations and clear instructions.&lt;/p&gt;

&lt;p&gt;You don't need to know the exact terminology. Ask "how do I make it louder?" and the AI understands you're asking about volume or gain. The AI knows which device you're asking about, knows the full context of the manual, and can reference specific sections and button combinations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Codes and Troubleshooting
&lt;/h2&gt;

&lt;p&gt;When your device shows an error code like E.05 or E.10, you need to know what it means and how to fix it. &lt;a href="https://www.teenagemanual.com" rel="noopener noreferrer"&gt;Teenage Manual&lt;/a&gt; has dedicated error code pages for devices that have them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/ep-133/errors" rel="noopener noreferrer"&gt;EP-133 error codes&lt;/a&gt; - Complete troubleshooting guide for E.01, E.02, E.03, E.05, E.10, E.11, E.12&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/ep-1320/errors" rel="noopener noreferrer"&gt;EP-1320 error codes&lt;/a&gt; - Fixes for E.01, E.02, E.03, E.04, E.05&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.teenagemanual.com/ep-40/errors" rel="noopener noreferrer"&gt;EP-40 error codes&lt;/a&gt; - Solutions for E.01, E.02, E.03, E.04, E.05&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each page shows what each error code means, whether it's a hardware issue or something you can fix yourself, and step-by-step instructions for fixing the fixable ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It's Better Than PDFs
&lt;/h2&gt;

&lt;p&gt;PDF manuals are designed for printing, not for finding information. When you're in the flow making music, you need quick answers, not a 144-page document to scroll through.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://www.teenagemanual.com" rel="noopener noreferrer"&gt;Teenage Manual&lt;/a&gt;, you stay in the flow. Ask a question, get an answer, keep making music. No switching apps, no scrolling, no frustration. The manual is always there, always searchable, always ready. No need to download PDFs or remember where you saved them.&lt;/p&gt;

&lt;p&gt;The most common questions people ask are things like "How do I make my first beat?" "What does the fader do?" "How do I sync with other devices?" These are basic questions that should be easy to answer. With PDFs, they're not. With &lt;a href="https://www.teenagemanual.com" rel="noopener noreferrer"&gt;Teenage Manual&lt;/a&gt;, they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pro Features
&lt;/h2&gt;

&lt;p&gt;The free version gives you full access to all manuals and unlimited questions. &lt;a href="https://www.teenagemanual.com/pro" rel="noopener noreferrer"&gt;Pro features&lt;/a&gt; ($5/month) add voice input for when your hands are on the device, saved answers to reference later, YouTube context to use tutorial transcripts as context for questions, and synced chat history across devices.&lt;/p&gt;

&lt;p&gt;The voice input is useful when you're working with hardware and your hands are busy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Usage
&lt;/h2&gt;

&lt;p&gt;Since launching, there have been 1,000+ users in the first week and 3,500+ questions asked. People are using &lt;a href="https://www.teenagemanual.com" rel="noopener noreferrer"&gt;Teenage Manual&lt;/a&gt; instead of Googling everything or scrolling through PDFs.&lt;/p&gt;

&lt;p&gt;Check out the &lt;a href="https://www.teenagemanual.com/love" rel="noopener noreferrer"&gt;wall of love&lt;/a&gt; to see what users are saying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;If you own a Teenage Engineering device, give it a try. Go to your device's ask page - for example, &lt;a href="https://www.teenagemanual.com/ep-133/ask" rel="noopener noreferrer"&gt;EP-133 ask&lt;/a&gt;, &lt;a href="https://www.teenagemanual.com/op-1/ask" rel="noopener noreferrer"&gt;OP-1 ask&lt;/a&gt;, &lt;a href="https://www.teenagemanual.com/op-xy/ask" rel="noopener noreferrer"&gt;OP-XY ask&lt;/a&gt;, or &lt;a href="https://www.teenagemanual.com/ep-1320/ask" rel="noopener noreferrer"&gt;EP-1320 ask&lt;/a&gt; - and ask a question. Try "How do I make my first beat?" or "What are the tape tricks?" or "How do I sync with other devices?"&lt;/p&gt;

&lt;p&gt;You'll get direct answers from the official manual, with proper button references and clear instructions. No scrolling, no guessing, no context switching.&lt;/p&gt;

&lt;p&gt;If you're seeing an error code, check the error code pages. For &lt;a href="https://www.teenagemanual.com/ep-133/errors" rel="noopener noreferrer"&gt;EP-133 errors&lt;/a&gt;, &lt;a href="https://www.teenagemanual.com/ep-1320/errors" rel="noopener noreferrer"&gt;EP-1320 errors&lt;/a&gt;, or &lt;a href="https://www.teenagemanual.com/ep-40/errors" rel="noopener noreferrer"&gt;EP-40 errors&lt;/a&gt;, you'll find what each code means and how to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I'm planning to open source the manual processing so the community can contribute, add more devices as Teenage Engineering releases new products, improve the manual view with better organization and navigation, and add video tutorials to integrate YouTube tutorials as learning resources.&lt;/p&gt;

&lt;p&gt;You can follow the &lt;a href="https://www.teenagemanual.com/changelog" rel="noopener noreferrer"&gt;changelog&lt;/a&gt; to see what's new.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://www.teenagemanual.com" rel="noopener noreferrer"&gt;Teenage Manual&lt;/a&gt; is the best way to access Teenage Engineering manuals. It's searchable, conversational, and actually usable. If you own a Teenage Engineering device, give it a try. Check out the &lt;a href="https://www.teenagemanual.com/pro" rel="noopener noreferrer"&gt;pro page&lt;/a&gt; to see all the features.&lt;/p&gt;

&lt;p&gt;Instead of scrolling through PDFs, you can ask questions and get direct answers from the official documentation. That's what makes it the best way to use Teenage Engineering manuals.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>documentation</category>
      <category>productivity</category>
      <category>tooling</category>
    </item>
    <item>
      <title>AI Deleted My Code: How to Recover</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Mon, 22 Dec 2025 14:07:22 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/ai-deleted-my-code-how-to-recover-18da</link>
      <guid>https://dev.to/ivandotcodes/ai-deleted-my-code-how-to-recover-18da</guid>
      <description>&lt;p&gt;It happens fast. You're iterating with Cursor or Claude Code, things are going well, and then the AI decides your codebase needs "cleaning up." Suddenly, files are gone. Maybe it consolidated what it thought were duplicates. Maybe it removed "unused" code that was actually imported elsewhere. Maybe it just hallucinated that a file shouldn't exist.&lt;/p&gt;

&lt;p&gt;If you're reading this, it probably just happened to you. Let's get your code back, and then make sure it never happens again.&lt;/p&gt;

&lt;h2&gt;
  
  
  First: Don't Close Anything
&lt;/h2&gt;

&lt;p&gt;Your editor likely still has deleted files in memory. Every tab you close is potential recovery lost. Keep everything open while you work through this.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check Your Editor's Undo History
&lt;/h2&gt;

&lt;p&gt;Start with the obvious. If the deleted file's tab is still open, try &lt;code&gt;Ctrl+Z&lt;/code&gt; (or &lt;code&gt;Cmd+Z&lt;/code&gt; on Mac) repeatedly. Sometimes the file content is recoverable even after the file itself was deleted from disk.&lt;/p&gt;

&lt;p&gt;This doesn't always work, especially for files the AI deleted without opening, but it's worth trying first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use Local History
&lt;/h2&gt;

&lt;p&gt;Both VS Code and Cursor maintain a local history of file changes that most developers don't know about. Press &lt;code&gt;Ctrl+Shift+P&lt;/code&gt; (or &lt;code&gt;Cmd+Shift+P&lt;/code&gt;), type "Local History: Find Entry to Restore", and browse for your deleted files.&lt;/p&gt;

&lt;p&gt;This works even for files that were deleted, as long as they were edited at some point. It's saved me more times than I'd like to admit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check Your AI Tool's Recovery Features
&lt;/h2&gt;

&lt;p&gt;If you're using Cursor, look for "Checkpoint created" messages in the Composer panel. Each prompt creates a checkpoint you can restore to. Click "Restore" next to the one from before the deletion.&lt;/p&gt;

&lt;p&gt;For Claude Code, press &lt;code&gt;Esc&lt;/code&gt; twice or type &lt;code&gt;/rewind&lt;/code&gt; to open the checkpoint menu. You can restore just the code, just the conversation, or both.&lt;/p&gt;

&lt;p&gt;The limitation with these is that they're tied to conversation context. If you've done a lot of work since the deletion, or if the deletion happened through Tab completions rather than Composer, you might not have a useful checkpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fall Back to Git
&lt;/h2&gt;

&lt;p&gt;If you committed before the AI made its changes, Git is your friend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git status           &lt;span class="c"&gt;# See what's been deleted&lt;/span&gt;
git checkout &lt;span class="nb"&gt;.&lt;/span&gt;       &lt;span class="c"&gt;# Restore everything to the last commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you didn't commit... well, that's the problem, isn't it? Git can only restore what you remembered to save.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Nothing Works
&lt;/h2&gt;

&lt;p&gt;If none of the above recovered your files, check your system backups. Time Machine on Mac, File History on Windows, or cloud sync services like Dropbox often keep recent versions. It's a long shot, but sometimes it works.&lt;/p&gt;

&lt;p&gt;If you've exhausted all options and your code is truly gone, you've learned an expensive lesson about working with AI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing This Next Time
&lt;/h2&gt;

&lt;p&gt;The fundamental problem is that AI agents are unpredictable. You can write better prompts, you can be more careful about reviewing diffs, but you can't eliminate the risk entirely. AI will occasionally delete things you didn't want deleted.&lt;/p&gt;

&lt;p&gt;The solution is having recovery baked in, not bolted on.&lt;/p&gt;

&lt;p&gt;Git works, but it requires discipline. You have to commit before every AI interaction, which breaks your flow when you're iterating quickly. And the one time you forget is the time the AI goes haywire.&lt;/p&gt;

&lt;p&gt;This is why we built &lt;a href="https://getmrq.com" rel="noopener noreferrer"&gt;mrq&lt;/a&gt;. It watches your project directory and captures every file change automatically. No commits, no decisions, no interruptions. You just run &lt;code&gt;mrq watch&lt;/code&gt; and forget about it.&lt;/p&gt;

&lt;p&gt;When an AI deletes something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mrq &lt;span class="nb"&gt;history&lt;/span&gt;        &lt;span class="c"&gt;# See recent snapshots&lt;/span&gt;
mrq restore abc123 &lt;span class="c"&gt;# Go back to any point&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're back to exactly where you were, even if you never committed. Even if the deletion happened three AI interactions ago. Every change is captured, every state is recoverable.&lt;/p&gt;

&lt;p&gt;The reality of AI coding is that things break unexpectedly. The question is whether you're prepared when they do.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://getmrq.com" rel="noopener noreferrer"&gt;mrq&lt;/a&gt; captures every file change automatically while you code with AI. Set it once, recover instantly when things break.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vibecoding</category>
      <category>programming</category>
      <category>cursor</category>
    </item>
    <item>
      <title>Introducing Automatic Security Audits for AI</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Sat, 20 Dec 2025 18:09:47 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/introducing-automatic-security-audits-for-ai-45eh</link>
      <guid>https://dev.to/ivandotcodes/introducing-automatic-security-audits-for-ai-45eh</guid>
      <description>&lt;p&gt;AI coding assistants move fast. They can refactor modules, add dependencies, and wire up integrations before you've finished reading the diff. That speed is valuable, but security issues can slip in just as quickly: hardcoded API keys, string-concatenated SQL queries, debug endpoints that never get removed.&lt;/p&gt;

&lt;p&gt;So we built something to help catch these issues early.&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%2Flrs44ps06zl2g8mrnet0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flrs44ps06zl2g8mrnet0.png" alt=" " width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://getmrq.com" rel="noopener noreferrer"&gt;mrq&lt;/a&gt; now scans your code changes for common security vulnerabilities automatically. Every snapshot triggers a background analysis looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hardcoded secrets, API keys, and credentials&lt;/li&gt;
&lt;li&gt;SQL injection patterns&lt;/li&gt;
&lt;li&gt;XSS vulnerabilities
&lt;/li&gt;
&lt;li&gt;Exposed sensitive data&lt;/li&gt;
&lt;li&gt;Insecure configurations&lt;/li&gt;
&lt;li&gt;Debug code that shouldn't ship&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results appear in your dashboard. If something gets flagged, you'll see a warning on that snapshot with details about what was found and where.&lt;/p&gt;

&lt;p&gt;&lt;a href="/security-audit.png" class="article-body-image-wrapper"&gt;&lt;img src="/security-audit.png" alt="Security audit warning in the mrq dashboard"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Issues are displayed inline with your snapshot history.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why It Matters
&lt;/h2&gt;

&lt;p&gt;Traditional security scanning happens in CI/CD, often hours after code was written. By then, the context is gone.&lt;/p&gt;

&lt;p&gt;With mrq, feedback is immediate. You're still in the middle of your session when an issue gets flagged. You can fix it while everything is fresh.&lt;/p&gt;

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

&lt;p&gt;This is experimental. Security analysis is hard, and we're using AI to identify patterns. There will be false positives and false negatives. This is not a replacement for proper security audits, code review, or dedicated scanning tools in your pipeline.&lt;/p&gt;

&lt;p&gt;Think of it as an early warning system. A nudge to double-check something before it gets committed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Availability
&lt;/h2&gt;

&lt;p&gt;Security audits are available on paid plans (Starter, Pro, Team). The feature runs automatically with no configuration needed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dashboard.getmrq.com" rel="noopener noreferrer"&gt;Try it out →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vibecoding</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>How to Recover AI Coding Mistakes</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Fri, 19 Dec 2025 23:10:36 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/how-to-recover-ai-coding-mistakes-26lf</link>
      <guid>https://dev.to/ivandotcodes/how-to-recover-ai-coding-mistakes-26lf</guid>
      <description>&lt;p&gt;If you've spent any significant time coding with AI assistants, you've probably had that moment where something goes wrong. Maybe the AI overwrote a file you needed, or confidently refactored something in a way that broke half your tests, or went down a rabbit hole that left your codebase in a confusing state.&lt;/p&gt;

&lt;p&gt;AI tools are genuinely useful for accelerating development. Their capability to make sweeping changes quickly can occasionally work against you though. Here's how we think about preventing and recovering from these situations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Recovery Problem
&lt;/h2&gt;

&lt;p&gt;AI coding assistants can modify code in ways that are hard to track manually. A single prompt might touch a dozen files. The AI doesn't hesitate or ask for confirmation the way a human collaborator might. Changes happen fast, sometimes faster than you can review them.&lt;/p&gt;

&lt;p&gt;AI assistants are actually quite capable and reliable for most tasks. The challenge is that their speed and confidence can outpace human review capacity, especially during longer sessions. You need a way to capture state continuously so you can always get back to where you were.&lt;/p&gt;

&lt;h2&gt;
  
  
  How &lt;a href="https://getmrq.com" rel="noopener noreferrer"&gt;mrq&lt;/a&gt; Handles This
&lt;/h2&gt;

&lt;p&gt;We built &lt;a href="https://getmrq.com" rel="noopener noreferrer"&gt;mrq&lt;/a&gt; specifically for this workflow. It runs in the background while you work with AI assistants and automatically captures snapshots when meaningful changes happen. You don't need to think about version control during your session, just focus on the work.&lt;/p&gt;

&lt;p&gt;Getting started is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; mrq-cli@latest
mrq login
mrq watch &lt;span class="nt"&gt;--daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the daemon is running, every significant change to your codebase is captured automatically. You can see your snapshot history anytime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;mrq &lt;span class="nb"&gt;history

&lt;/span&gt;Recent Snapshots
────────────────────────────────────────────────────
  10:47:23  abc123  Added OAuth provider config and login route
                    +142 lines, &lt;span class="nt"&gt;-3&lt;/span&gt; lines across 4 files

  10:43:51  def456  Refactored auth middleware to support JWT
                    +87 lines, &lt;span class="nt"&gt;-45&lt;/span&gt; lines across 2 files

  10:41:02  ghi789  Fixed token validation edge &lt;span class="k"&gt;case&lt;/span&gt;
                    +12 lines, &lt;span class="nt"&gt;-8&lt;/span&gt; lines across 1 file

  10:38:15  jkl012  Initial auth module setup
                    +234 lines across 6 files
────────────────────────────────────────────────────
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each snapshot includes an AI-generated summary describing what changed, so you can quickly scan and find the point you want to return to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instant Recovery
&lt;/h2&gt;

&lt;p&gt;When something goes wrong, recovery is a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mrq restore def456
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your current state is automatically backed up before the restore, so if you change your mind, you can get back to where you were. There's no hunting through history or trying to remember when things last worked. You just pick a snapshot and restore.&lt;/p&gt;

&lt;p&gt;The dashboard at &lt;a href="https://dashboard.getmrq.com" rel="noopener noreferrer"&gt;dashboard.getmrq.com&lt;/a&gt; gives you a visual interface for browsing snapshots, comparing changes between any two points, and viewing the actual diffs. This is particularly helpful when you want to understand what the AI changed before deciding whether to restore.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working Confidently with AI
&lt;/h2&gt;

&lt;p&gt;The bigger benefit is psychological. When you know you can always recover, you're more willing to let the AI try ambitious things. Want to see if it can refactor your entire auth system? Go for it. If it doesn't work out, you're one command away from being back where you started.&lt;/p&gt;

&lt;p&gt;This changes how you work with AI assistants. Instead of carefully scoping prompts to minimize risk, you can be more exploratory. Try the aggressive approach first. If it works, great. If not, restore and try something more conservative.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Note on Git
&lt;/h2&gt;

&lt;p&gt;You might wonder why not just use git for this. Git is excellent for deliberate, discrete commits with meaningful messages for collaboration and code review. &lt;/p&gt;

&lt;p&gt;The challenge is that git requires intentional action to capture state. During rapid AI-assisted iteration, stopping to commit breaks your flow. Most developers either skip commits during these sessions (and lose their safety net) or create noisy checkpoint commits that clutter history.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://getmrq.com" rel="noopener noreferrer"&gt;mrq&lt;/a&gt; complements git. Use &lt;a href="https://getmrq.com" rel="noopener noreferrer"&gt;mrq&lt;/a&gt; for the messy exploration phase, then commit to git when you're happy with where things landed. You get continuous protection during development without sacrificing a clean git history for collaboration.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://getmrq.com" rel="noopener noreferrer"&gt;mrq&lt;/a&gt; is currently in open beta with a free tier that covers most individual developer needs. If you've experienced the frustration of losing AI-assisted work, or find yourself holding back on experiments because recovery feels uncertain, give it a try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; mrq-cli@latest
mrq login  
mrq watch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'd love to hear how it works for your workflow.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>vibecoding</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Build &amp; Deploy AI-Powered Web Services from a Single Prompt</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Tue, 16 Jul 2024 17:29:32 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/build-deploy-ai-powered-web-services-from-a-single-prompt-ooo</link>
      <guid>https://dev.to/ivandotcodes/build-deploy-ai-powered-web-services-from-a-single-prompt-ooo</guid>
      <description>&lt;p&gt;At Shuttle, we've been working on a new tool that we think could change how developers approach AI integration. We're calling it ShuttleAI, and it allows you to build and deploy AI-powered web services from a single prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's the TL;DR:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Describe your AI service in plain language&lt;/li&gt;
&lt;li&gt;ShuttleAI generates a project spec for you to review&lt;/li&gt;
&lt;li&gt;Approve or modify the spec&lt;/li&gt;
&lt;li&gt;ShuttleAI creates the project files&lt;/li&gt;
&lt;li&gt;You can prompt for changes or deploy&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's that simple. But let's dig into the details.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: AI Integration is Hard
&lt;/h2&gt;

&lt;p&gt;If you've ever tried to integrate AI into a web service, you know it's not trivial. Here are some common challenges:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Complexity&lt;/strong&gt;: AI frameworks often require specialized knowledge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time&lt;/strong&gt;: Setting up AI services can take weeks or months.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure&lt;/strong&gt;: Managing AI models needs robust, scalable infrastructure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ongoing maintenance&lt;/strong&gt;: AI services require continuous monitoring and updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These barriers can be significant, especially for smaller teams or developers new to the noisy AI space.&lt;/p&gt;

&lt;h2&gt;
  
  
  How ShuttleAI Works
&lt;/h2&gt;

&lt;p&gt;ShuttleAI aims to simplify this process dramatically. Here's a step-by-step breakdown:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Describe Your Service&lt;/strong&gt;: You provide a prompt describing the AI service you want to build. For example:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Build a web service that takes weather forecast data and user profiles as input, then returns personalized weather recommendations."
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Review the Spec&lt;/strong&gt;: ShuttleAI generates a project specification document in markdown. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API endpoints&lt;/li&gt;
&lt;li&gt;Data models&lt;/li&gt;
&lt;li&gt;AI model selection&lt;/li&gt;
&lt;li&gt;Infrastructure requirements&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can review and modify this spec as needed.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Generate Project Files&lt;/strong&gt;: Once you approve the spec, ShuttleAI creates all necessary project files. This includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backend code (eg. Python with Flask)&lt;/li&gt;
&lt;li&gt;AI model integration code&lt;/li&gt;
&lt;li&gt;Infrastructure in the form of &lt;a href="https://docs.shuttle.rs/introduction/how-shuttle-works" rel="noopener noreferrer"&gt;Infrastructure from Code&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Iterative Refinement&lt;/strong&gt;: You can prompt ShuttleAI to make changes at this stage. For example:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Add rate limiting to the API endpoints"
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;ShuttleAI will update the project files accordingly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deploy&lt;/strong&gt;: Once you're satisfied, ShuttleAI compiles and deploys your project on the Shuttle platform.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;p&gt;We're excited to see what developers will build with ShuttleAI. Here are a few ideas we've been thinking about:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Personalized Content Engines&lt;/strong&gt;: Analyze user behavior and content metadata to provide tailored recommendations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intelligent Data Processing&lt;/strong&gt;: Create services that clean, normalize, and enrich data using AI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Natural Language Interfaces&lt;/strong&gt;: Build APIs that can understand and respond to natural language queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Predictive Analytics Services&lt;/strong&gt;: Develop APIs that forecast trends based on historical data.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Beta Testing and Early Access
&lt;/h2&gt;

&lt;p&gt;ShuttleAI is still in development, and we're looking for beta testers. If you're interested in being one of the first to try it out, we're offering early access to the first 100 developers who sign up for our waitlist.&lt;/p&gt;

&lt;p&gt;As a beta tester, you'll get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Early access to ShuttleAI&lt;/li&gt;
&lt;li&gt;Direct support from our development team&lt;/li&gt;
&lt;li&gt;The opportunity to shape the future of the tool&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://shuttle.rs/ai" rel="noopener noreferrer"&gt;Click here to sign up for early access!&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;We're continuously working on improving ShuttleAI. Some features we're exploring for future releases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Support for more AI models and APIs&lt;/li&gt;
&lt;li&gt;Advanced customization options for generated services&lt;/li&gt;
&lt;li&gt;A marketplace for sharing and deploying AI service templates&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  We Want Your Feedback
&lt;/h2&gt;

&lt;p&gt;ShuttleAI is still evolving, and we want to build it in a way that truly serves developers' needs. If you have ideas, questions, or concerns, we want to hear them.&lt;/p&gt;

&lt;p&gt;Drop us a line at &lt;a href="mailto:hello@shuttle.rs"&gt;hello@shuttle.rs&lt;/a&gt; or open an issue in our &lt;a href="https://github.com/shuttle-hq/shuttle" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Remember, the first 100 signups get early access to the beta. Don't miss out on the chance to shape the future of AI service development!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://shuttle.rs/ai" rel="noopener noreferrer"&gt;Click here to sign up for early access!&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>rust</category>
      <category>python</category>
    </item>
    <item>
      <title>Rethinking Virtualization for Backends</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Thu, 27 Oct 2022 20:16:38 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/rethinking-virtualization-for-backends-aj1</link>
      <guid>https://dev.to/ivandotcodes/rethinking-virtualization-for-backends-aj1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TLDR&lt;/strong&gt;&lt;br&gt;
Virtual machines and containers have improved backends in a lot of ways, but over time they have also created a lot of problems. We believe it's time to rethink how we use virtualization for backend development.&lt;/p&gt;

&lt;p&gt;We're building a backend framework that shifts the scope of virtualization from processes down to service components.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In web applications nowadays, you can sort any component somewhere in a broad spectrum from client-side to server-side.&lt;/p&gt;

&lt;p&gt;On the client-side, there's everything that runs on people's devices, most likely a browser or an app. On the server-side, there's everything that runs in the cloud. That includes databases, authentication management, batch jobs, events handling etc.&lt;/p&gt;

&lt;p&gt;Each web framework squarely fits somewhere on that line. React, the most popular front-end web framework out there, is wholly client-side. Express, one of the most popular backend web frameworks, is wholly server-side.&lt;/p&gt;

&lt;p&gt;Client-side has been historically dominated by JavaScript frameworks. This is not surprising since every client ships a powerful JavaScript engine and that is the best way to make a web page interactive.&lt;/p&gt;

&lt;p&gt;On the server-side, things are more fragmented. This is also not surprising: backend services are just plain native processes that use their environment's network stack to respond to requests. And there is a world of different ways to write and run these: literally the history of computing.&lt;/p&gt;

&lt;p&gt;As in many other scenarios in software engineering and computer science, this huge free space of options is also the cause of a lot of problems. To understand why, we need to talk about containers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Containers are a solution and a problem
&lt;/h2&gt;

&lt;p&gt;On its way to settling in its standards, the cloud - epitomized by AWS - has evolved massively over the past decade. My co-founder has written a &lt;a href="https://www.shuttle.rs/blog/2022/05/09/ifc" rel="noopener noreferrer"&gt;post on this&lt;/a&gt; previously.&lt;/p&gt;

&lt;p&gt;Today we, as software engineers, deal with it as it is: the result of incremental changes on top of a status quo. And it is not ideal.&lt;/p&gt;

&lt;p&gt;What starts life as physical machines in a data center gets split up into tens, sometimes hundreds, of virtual machines in the AWS console. But VMs are heavy, slow to start and it's difficult to make a lot of them coexist without wasting resources like RAM and storage.&lt;/p&gt;

&lt;p&gt;Then came along containers. Building on top of the Linux kernel's namespacing features, they made images smaller and runtimes more efficient than VMs. The genius of it is to move the virtualization layer from the hardware - where the kernel itself runs virtualized - to the software - where only processes run "virtualized". With containers, virtualized processes run natively in the host kernel, like any other. Except that their I/Os are carefully kept segregated from others in the host system. Any bit of compiled code that is executable on the host can be run in a container. And you can run processes in a container without a separate boot sequence and a full-fledged virtualized operating system with its own heavy machinery like a scheduler and dedicated virtualized hardware.&lt;/p&gt;

&lt;p&gt;Containers are actually much older than a lot of people realise, going as far as 2008 with LXC in Linux’s case (even more in the case of FreeBSD). Their popularity, however, really took off with the arrival of Docker. The execution of Docker as a platform-as-a-service product was so good it took over software engineering practices for the following decade. And it is still the gold standard today in terms of usage.&lt;/p&gt;

&lt;p&gt;Of course, companies were quick to build products on top of containers. They basically pass through the benefits of containers to their paying customers. Heroku is one of the most notable example. And while containers delivered most of us, directly or indirectly, from having to deal with VMs as a unit of deployment, they certainly have their issues. The biggest one being their size.&lt;/p&gt;

&lt;p&gt;VMs have to run an entire operating system, containers don't. So they're quite a lot smaller. But container images still have to contain enough userspace to make the things you want to run actually runnable. For the way most people use them in deployments of web apps, this is generally still quite a lot!&lt;/p&gt;

&lt;p&gt;The heavier your containers are, the more difficult everything else becomes. They take longer to build, they need more resources to run, they are more expensive to store, etc.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://shuttle.rs/" rel="noopener noreferrer"&gt;shuttle&lt;/a&gt; we're convinced that a lot of the pains experienced by software engineers in the post-Docker world can be traced back to that very simple statement: containers are often too heavy for the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replacing containers
&lt;/h2&gt;

&lt;p&gt;You're probably thinking: it's nice and optimistic to say containers are too heavy, but what do you replace them with?&lt;/p&gt;

&lt;p&gt;Well first, as an open-source company, you avoid making the same mistake Docker made. If you make the scope of virtualization too broad, you will end up with the same result as containers. The root cause behind the heavy weight of containers is that they have been built for too many usecases. They layer virtualization on top of &lt;em&gt;all&lt;/em&gt; the I/Os of a native Linux process: their usecase is just about anything that runs.&lt;/p&gt;

&lt;p&gt;We’re concerned with the backend services most people write. These are HTTP request/response handlers, with or without state. And for that specific usecase, most projects just end up worse off by handing over backend services as container images to their deployment platform of choice.&lt;/p&gt;

&lt;p&gt;So we need to restrict the scope of virtualization to something more specific to web app backends. This is a trade-off of course, like most things in software engineering. By restricting the scope of a tool, you lose the ability to do certain things. But like most of these trade-offs, you usually are better served by erring on the side of simplicity unless you have specific needs that require extra complexity. In other words: use heavy machinery when you actually have a need for it, not before.&lt;/p&gt;

&lt;p&gt;Where does that leave us then? We need a new take on virtualization. One that has, perhaps, simplified I/Os and is engineered for backend services. Thankfully, we don't have to invent most of that wheel: let's talk about WASI.&lt;/p&gt;

&lt;h2&gt;
  
  
  WASM and WASI
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If WASM+WASI existed in 2008, we wouldn't have needed to created Docker. That's how important it is. Webassembly on the server is the future of computing. A standardized system interface was the missing link. Let's hope WASI is up to the task! &lt;a href="https://t.co/wnXQg4kwa4" rel="noopener noreferrer"&gt;https://t.co/wnXQg4kwa4&lt;/a&gt;&lt;/p&gt;— Solomon Hykes (@solomonstre) &lt;a href="https://twitter.com/solomonstre/status/1111004913222324225?ref_src=twsrc%5Etfw" rel="noopener noreferrer"&gt;March 27, 2019&lt;/a&gt;
&lt;/blockquote&gt; 

&lt;p&gt;&lt;a href="http://webassembly.org/" rel="noopener noreferrer"&gt;WebAssembly&lt;/a&gt; (abbreviated WASM) is an instruction set for extremely lightweight virtual machines. Its most common use is to speed up client-side interactivity. This is made possible as popular browsers have rolled out WASM runtimes a few years back.&lt;/p&gt;

&lt;p&gt;WASM is made for fast sandboxing. However, without any extension, it is unable to perform even simple I/O operations like reading data from a file descriptor. This is not a big deal if WASM is used &lt;em&gt;in the browser&lt;/em&gt; - we definitely don't want to let browsers freely provide file system access to web apps. But it is a serious limitation if WASM is to be used server-side - how else are you going to serve endpoints without that?&lt;/p&gt;

&lt;p&gt;Therefore, the introduction of WASM was followed, a short while later, by WASI - the &lt;a href="https://wasi.dev" rel="noopener noreferrer"&gt;WebAssembly System Interface&lt;/a&gt;. WASI is a standard API to give WASM code the ability to do system-level I/O. This allows WASM code running in a WASI-compliant runtime to do a lot of what a native process can do through syscalls.&lt;/p&gt;

&lt;p&gt;The really powerful thing about WASM is that it is a very common compilation target. Major languages (and commonly associated frameworks) now support building WASM as a target, just the same way you build for amd64 or arm. And a lot of standard libraries have added support for WASI-based I/Os.&lt;/p&gt;

&lt;p&gt;This is what Docker's founder had to say about WASI, back in 2019. And we agree with them. At the end of the day containers are, really, just I/O-level virtualization. Now, a few years after its initial introduction, WASM runtimes have stabilised their support of WASI. This creates a prime environment to engineer, on top of WASI, a solution to containers' biggest drawbacks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changing virtualization for backends
&lt;/h2&gt;

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

&lt;p&gt;When we launched &lt;a href="https://shuttle.rs/" rel="noopener noreferrer"&gt;shuttle&lt;/a&gt; for its early alpha, back in March 2022, our purpose was to address the issues people face when building and deploying web app backends. So we created an open-source infrastructure-from-code platform with which you don’t need to write Containerfiles and orchestrate images, starting with support for Rust.&lt;/p&gt;

&lt;p&gt;Since then, more than 1.2k people starred the &lt;a href="https://github.com/shuttle-hq/shuttle" rel="noopener noreferrer"&gt;shuttle repo&lt;/a&gt; and hundreds joined our discord community. And we've seen more than 2000 deployments and hundreds of users! From which we received a ton of feedback.&lt;/p&gt;

&lt;p&gt;What we quickly realized is that while we simplified the process of getting started implementing your own backend and setting up its infrastructure, we completely failed to solve two core problems: long build and deploy times.&lt;/p&gt;

&lt;p&gt;Rust has notoriously long build times (this probably has to do with static linking and heavy reliance on compile-time code generation). And while it supports incremental compilation out of the box, in a containerized environment, missing the cache for an image layer means having to rebuild from scratch.&lt;/p&gt;

&lt;p&gt;We've found that no matter how much we tweaked our internal caching, too often users had to wait too long for their projects to build and deploy - something that can take minutes in the simplest projects, and closer to half an hour in complex ones. The reason was simple: our execution of our idea for shuttle is built on top of containers. And no matter how much we try to distance containers from our users, their limitations always surface back.&lt;/p&gt;

&lt;p&gt;It was time for a complete rethink, so we took a radical view: let's start from the services people are writing, distilling what they need done quickly and easily. And let's make it our mission to optimize the hell out of the entire stack. We thought that if the execution of that idea is done right, it'd let us trim the dependency tree of services our users deploy and slim the runtime that every service ships with.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What we quickly realized is that while we trimmed down the process of getting started implementing your own backend and setting up its infrastructure, we completely failed to solve two core problems: long build and deploy times.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After all, a major culprit of these long build and deploy times in the real world is the large number of heavy dependencies of even simple projects. There's not much you can do about this: most services have a pretty big runtime that includes heavy machinery like an asynchronous executor (e.g. &lt;a href="https://tokio.rs" rel="noopener noreferrer"&gt;tokio&lt;/a&gt;), a web server (e.g. &lt;a href="https://github.com/hyperium/hyper" rel="noopener noreferrer"&gt;hyper&lt;/a&gt;), database drivers (e.g. &lt;a href="https://github.com/launchbadge/sqlx" rel="noopener noreferrer"&gt;sqlx&lt;/a&gt;) and more. And on every deploy you need to re-build them and hope artifact caches are hit in order to get an incremental build. And it's not just building either, the running time of tests is also impacted by this. The closure of the codebase you're engaging in those tests is very large indeed as it follows that of your dependencies.&lt;/p&gt;

&lt;p&gt;This stuff materializes itself everywhere. Just try taking this hello world snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;axum&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_hello&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;'static&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s"&gt;"You're slow, Heroku!"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[tokio::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"PORT"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_hello&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;.into_make_service&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nn"&gt;hyper&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"127.0.0.1:{port}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="nf"&gt;.serve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and deploy it to Heroku:&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.shuttle.rs%2Fimages%2Fblog%2Faxum-heroku.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%2Fwww.shuttle.rs%2Fimages%2Fblog%2Faxum-heroku.gif" alt="Deployment demo" width="8" height="5"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To try to address this, we wanted to &lt;strong&gt;move all these heavy dependencies to a common runtime across services&lt;/strong&gt;. So your tokio, hyper, sqlx and co (in the case of Rust), now all belong to a long-lived containerized process running persistently in the cloud. Whereas all your service logic, database and endpoint code build into lightweight WASM modules that are dynamically loaded in-place by this global persistent process. That way "building" means compiling a very lightweight codebase with a small dependency footprint. And "deploying" means calling upon the control plane of that long-lived process to replace service components without rolling out new images, containers or VMs.&lt;/p&gt;

&lt;p&gt;This leaves us with a trimmed down user-facing API that still uses familiar objects like &lt;code&gt;PgClient&lt;/code&gt;s and axum-style routes with guards:&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%2Fpaosvnqts64cx33hbhdh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpaosvnqts64cx33hbhdh.png" alt="Beta API" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Except that now the virtualization platform in which your services are run is responsible for instantiating these objects and calling these functions.&lt;/p&gt;

&lt;p&gt;With this approach, the component of virtualization that you end up deploying on a daily basis is much smaller than traditional VMs and containers. In a way we can say this makes the virtualization layer more adapted to the specific needs of backend services running in the cloud. It's an optimized I/O surface between backend service components that change a lot (e.g. endpoint implementations) and their environing long-lived runtimes that don't (e.g. tokio/hyper/sqlx).&lt;/p&gt;

&lt;p&gt;This results in "images" that are effectively up to &lt;strong&gt;100x smaller&lt;/strong&gt; because of the switch from container images to WASM binaries. And super fast to deploy too, from tens of minutes sometimes to &lt;strong&gt;less than a second&lt;/strong&gt; all the time. All because when things are &lt;em&gt;really&lt;/em&gt; incremental, you don't have to build and test a large codebase with its large userspace dependencies on every push. You just need to build and test the code you're writing and the changes you've made.&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%2Fn040scsdi5c5otr6rpjg.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%2Fn040scsdi5c5otr6rpjg.gif" alt="Deploy your app in less than a second" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our vision for this new way of doing backend development is shuttle-next: a next-generation backend framework with the fastest build, test and deployment times ever.&lt;/p&gt;

&lt;p&gt;We believe that scoping down virtualization to the level of service components will eventually become the norm for backend development. In the same way we all think it's often not best to setup and start a VM only to run a single process, we will eventually all think it's misguided to build and start a container only to run a single service.&lt;/p&gt;

&lt;p&gt;We are launching shuttle-next as part of our closed beta for shuttle later this month, with the public release coming soon after. If you’re keen to give it a try early, &lt;strong&gt;&lt;a href="https://shuttle.rs/beta" rel="noopener noreferrer"&gt;sign up for the beta!&lt;/a&gt;&lt;/strong&gt; We'd love to know what you think!&lt;/p&gt;

&lt;p&gt;In the meantime, check out &lt;a href="https://github.com/shuttle-hq/shuttle" rel="noopener noreferrer"&gt;shuttle's GitHub repo&lt;/a&gt; and &lt;a href="https://twitter.com/shuttle_dev" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; for updates. If you’d like to support us, please star the repo and/or join the &lt;a href="https://discord.gg/shuttle" rel="noopener noreferrer"&gt;shuttle Discord community&lt;/a&gt;!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>programming</category>
      <category>opensource</category>
      <category>rust</category>
    </item>
    <item>
      <title>Create and Deploy a Discord bot in Rust, for free</title>
      <dc:creator>Ivan</dc:creator>
      <pubDate>Thu, 15 Sep 2022 13:28:29 +0000</pubDate>
      <link>https://dev.to/ivandotcodes/create-and-deploy-a-discord-bot-in-rust-for-free-3203</link>
      <guid>https://dev.to/ivandotcodes/create-and-deploy-a-discord-bot-in-rust-for-free-3203</guid>
      <description>&lt;p&gt;In this post, we will look at a simple way to add custom functionality to a Discord server using a bot written in Rust. We will first register a bot with Discord, then go about how to create a Serenity application that will later run on shuttle. Finally, we will make the bot do something useful, writing some Rust code to get information from an external service.&lt;/p&gt;

&lt;p&gt;The full code can be found in &lt;a href="https://github.com/kaleidawave/discord-weather-bot" rel="noopener noreferrer"&gt;this repository&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registering our bot
&lt;/h3&gt;

&lt;p&gt;Before we start making our bot, we need to register it for Discord. We do that by going to &lt;a href="https://discord.com/developers/applications" rel="noopener noreferrer"&gt;https://discord.com/developers/applications&lt;/a&gt; and creating a new application.&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%2Far76rvjthqopg69anvu5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Far76rvjthqopg69anvu5.png" width="800" height="332"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application process is also used for adding functionality to Discord but we will be only using the bot offering. Fill in the basic details and you should get to the following 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%2Fozeowk5mhbdumu5vh6bw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fozeowk5mhbdumu5vh6bw.png" width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You want to copy the Application ID and have it handy, because we will use it to add our bot to a test server.&lt;/p&gt;

&lt;p&gt;Next, we want to create a bot. You can set its public username here:&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%2Fvn1y30e7gtx1zcis04wc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvn1y30e7gtx1zcis04wc.png" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You want to click the reset token and copy this value (we will use it in a later step). This value represents the username and password as a single value that Discord uses to authenticate that our server is the one controlling the bot. You want to keep this value secret.&lt;/p&gt;

&lt;p&gt;You also want to tick the &lt;code&gt;MESSAGE CONTENT INTENT&lt;/code&gt; setting so it can read the commands input.&lt;/p&gt;

&lt;p&gt;To add the bot to the server we will test on, we can use the following URL (replace &lt;code&gt;*application_id*&lt;/code&gt; in the URL with the ID you copied beforehand):&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="nx"&gt;https&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="c1"&gt;//discord.com/oauth2/authorize?client_id=*application_id*&amp;amp;scope=bot&amp;amp;permissions=8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we create it with &lt;code&gt;permissions=8&lt;/code&gt; so that it can do everything on the server. If you are adding to another server, select only the permissions it needs.&lt;/p&gt;

&lt;p&gt;We now have a bot on our server:&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%2Flu9igf6oian10xz2ipy8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flu9igf6oian10xz2ipy8.png" width="406" height="318"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Oh, they’re offline 😢&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting a bot online
&lt;/h2&gt;

&lt;p&gt;At this moment, our bot is not running because there is no code. We will have to write it and run it before we can start interacting with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://docs.rs/serenity/latest/serenity/index.html" rel="noopener noreferrer"&gt;Serenity&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Serenity is a library for writing Discord bots (and communicating with the Discord API). We can create a new Serenity project which is readily deployable on shuttle with: &lt;code&gt;cargo shuttle init --serenity&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;If you don’t have shuttle yet, you can install it with &lt;code&gt;cargo install cargo-shuttle&lt;/code&gt;. Afterwards, run the following in an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cargo shuttle init --serenity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running it you, should see the following generated in &lt;code&gt;src/lib.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="k"&gt;log&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serenity&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;async_trait&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serenity&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;model&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;channel&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serenity&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;model&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;gateway&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serenity&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;prelude&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;shuttle_service&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CustomError&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;shuttle_service&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SecretStore&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;sqlx&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;PgPool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Bot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[async_trait]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;EventHandler&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;Bot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="py"&gt;.content&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"!hello"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="py"&gt;.channel_id&lt;/span&gt;&lt;span class="nf"&gt;.say&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"world!"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nd"&gt;error!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Error sending message: {:?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{} is connected!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="py"&gt;.user.name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[shuttle_service::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;serenity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;#[shared::Postgres]&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PgPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;shuttle_service&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ShuttleSerenity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get the discord token set in `Secrets.toml` from the shared Postgres database&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;
        &lt;span class="nf"&gt;.get_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DISCORD_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CustomError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Set gateway intents, which decides what events the bot will be notified about&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;intents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;GatewayIntents&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GUILD_MESSAGES&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;GatewayIntents&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MESSAGE_CONTENT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.event_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Err creating client"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&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;
  
  
  Building an interaction for our bot
&lt;/h3&gt;

&lt;p&gt;We want to call our bot when chatting in a text channel. Discord enables this with &lt;a href="https://discord.com/blog/slash-commands-are-here" rel="noopener noreferrer"&gt;slash commands&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Slash commands can be server-specific (servers are named as &lt;code&gt;guilds&lt;/code&gt; in Discords API documentation) or application specific (across all servers the bot is in). For testing, we will only enable it on a single guild/server. This is because the application-wide commands can take an hour to fully register whereas the guild/server specific ones are instant, so we can test the new commands immediately.&lt;/p&gt;

&lt;p&gt;You can copy the guild ID by right-clicking here on the server name and click &lt;code&gt;copy ID&lt;/code&gt; (&lt;a href="https://www.howtogeek.com/714348/how-to-enable-or-disable-developer-mode-on-discord/" rel="noopener noreferrer"&gt;you will need developer mode enabled to do this&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%2Fee5ctz76r03f3dqviz60.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fee5ctz76r03f3dqviz60.png" width="371" height="569"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that we have the information for setup, we can start writing our bot and its commands.&lt;/p&gt;

&lt;p&gt;We will first get rid of the &lt;code&gt;async fn message&lt;/code&gt; hook as we won’t be using it in this example. In the &lt;code&gt;ready&lt;/code&gt; hook we will call &lt;code&gt;set_application_commands&lt;/code&gt; with a &lt;code&gt;GuildId&lt;/code&gt; to register a command with Discord. Here we register a &lt;code&gt;hello&lt;/code&gt; command with a description and no parameters (Discord refers to these as options).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[async_trait]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;EventHandler&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;Bot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{} is connected!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="py"&gt;.user.name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;guild_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GuildId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;your&lt;/span&gt; &lt;span class="n"&gt;guild&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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;commands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;GuildId&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;set_application_commands&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;guild_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="nf"&gt;.create_application_command&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="nf"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Say hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="nd"&gt;info!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{:#?}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;commands&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;blockquote&gt;
&lt;p&gt;Serenity has a bit of a different way of registering commands using a callback. If you are working on a larger command application, &lt;a href="https://docs.rs/poise/latest/poise/" rel="noopener noreferrer"&gt;poise&lt;/a&gt; (which builds on Serenity) might be better suited.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;With our command registered, we will now add a hook for when these commands are called using &lt;code&gt;interaction_create&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[async_trait]&lt;/span&gt;
&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;EventHandler&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;Bot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ready&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Ready&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;interaction_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Interaction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nn"&gt;Interaction&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;ApplicationCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;interaction&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="py"&gt;.data.name&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="s"&gt;"hello"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="nf"&gt;.to_owned&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;unreachable!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Unknown command: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;};&lt;/span&gt;

            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;create_interaction_response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
                &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="nf"&gt;.create_interaction_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;response&lt;/span&gt;
                        &lt;span class="nf"&gt;.kind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;InteractionResponseType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ChannelMessageWithSource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;.interaction_response_data&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="nf"&gt;.content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="p"&gt;});&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;why&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;create_interaction_response&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nd"&gt;eprintln!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cannot respond to slash command: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;why&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Trying it out
&lt;/h3&gt;

&lt;p&gt;Now with the code written we can test it locally. Before we do that we have to authenticate the bot with Discord. We do this with the value we got from "Reset Token" on the bot screen in one of the previous steps. To register a secret with shuttle we create a &lt;code&gt;Secrets.toml&lt;/code&gt; file with a key value pair. This pair is read by the &lt;code&gt;pool.get_secret("DISCORD_TOKEN")&lt;/code&gt; call in the &lt;code&gt;ready&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Secrets.toml
DISCORD_TOKEN="*your discord token*"
DISCORD_GUILD_ID="*the guild we are testing on*"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Currently &lt;em&gt;secrets&lt;/em&gt; are stored using shuttle's database Postgres offering (thus why the parameter on main is &lt;code&gt;PgPool&lt;/code&gt;). Therefore during local testing you need access to a Postgres database. Shuttle's local runner does this using Docker which will require &lt;a href="https://community.chocolatey.org/packages/docker-desktop/2.1.0.3" rel="noopener noreferrer"&gt;Docker desktop&lt;/a&gt; being locally installed to use the Secrets locally. &lt;strong&gt;This is is subject to change in the future so that it doesn't require Docker for local secrets.&lt;/strong&gt; There is also a known issue with deploying Secrets on Windows so if you have problems &lt;a href="https://discord.gg/shuttle" rel="noopener noreferrer"&gt;consult the Discord&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;cargo shuttle run&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;We should see that our bot now displays as online:&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%2Fqdsbtu2oikirqbz079k8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqdsbtu2oikirqbz079k8.png" width="691" height="197"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When typing, we should see our command come up with its description:&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%2Fqvj9ki5usupze14akuo8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqvj9ki5usupze14akuo8.png" width="800" height="202"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our bot should respond with "hello" to our command:&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%2Fgbhd3fv0f1r5bu25srjs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgbhd3fv0f1r5bu25srjs.png" width="800" height="181"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Wow! Let’s make our bot do something a little more useful.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making the bot do something
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/public-apis/public-apis" rel="noopener noreferrer"&gt;There is plenty of free APIs&lt;/a&gt; that can be used for getting information on a variety of topics.&lt;/p&gt;

&lt;p&gt;For this demo, we are going to build a bot that gives a forecast for a location. I used the &lt;a href="https://developer.accuweather.com/" rel="noopener noreferrer"&gt;AccuWeather API&lt;/a&gt; for this demo. If you are following this tutorial 1:1 you can go and register an application to get an access key. If you are using a different API this is still the sort of process you would follow.&lt;/p&gt;

&lt;p&gt;To get a forecast using the API requires two requests:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get a location ID for a named location&lt;/li&gt;
&lt;li&gt;Get the forecast at the location ID&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The API requires making network requests and it returns a JSON response. We can make the requests with &lt;code&gt;cargo add reqwest -F json&lt;/code&gt; and deserialize the results to structures using serde, with &lt;code&gt;cargo add serde&lt;/code&gt;. We will then have a function that chains the two requests together and deserializes the forecast to a readable result.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can skip some of the boilerplate by using &lt;a href="https://docs.rs/serde_json/latest/serde_json/#operating-on-untyped-json-values" rel="noopener noreferrer"&gt;direct access on untyped values&lt;/a&gt;. But we will opt for the better strongly typed structured approach.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here we type some of the structures returned by the API and add &lt;code&gt;#[derive(Deserialize)]&lt;/code&gt; so they can be decoded from JSON. All the keys are in &lt;em&gt;&lt;code&gt;PascalCase&lt;/code&gt;&lt;/em&gt; so we use the &lt;code&gt;#[serde(rename_all = "PascalCase")]&lt;/code&gt; helper attribute to stay aligned with Rust standards. Some are completely different from the Rust field name so we use &lt;code&gt;#[serde(alias = ...)]&lt;/code&gt; on the field to set its matching JSON representation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In weather.rs&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;serde&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;Debug)]&lt;/span&gt;
&lt;span class="nd"&gt;#[serde(rename_all&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PascalCase"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;localized_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Country&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Display&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;Location&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Formatter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&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;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;write!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"{}, {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.localized_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.country.id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;Debug)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Country&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[serde(alias&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ID"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;Debug)]&lt;/span&gt;
&lt;span class="nd"&gt;#[serde(rename_all&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"PascalCase"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Forecast&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Headline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Deserialize,&lt;/span&gt; &lt;span class="nd"&gt;Debug)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Headline&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;#[serde(alias&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Text"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="n"&gt;overview&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The above skips &lt;a href="https://developer.accuweather.com/accuweather-forecast-api/apis/get/forecasts/v1/daily/1day/%7BlocationKey%7D" rel="noopener noreferrer"&gt;a lot of the fields returned by the API&lt;/a&gt;, only opting for the ones we will use in this demo. If you wanted to type all the fields you could try the new &lt;a href="https://rust-analyzer.github.io/thisweek/2022/08/15/changelog-142.html#new-features" rel="noopener noreferrer"&gt;type from JSON feature in rust-analyzer&lt;/a&gt; to avoid having to write as much.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our location request call also fails if the search we put in returns no places. We will create an intermediate type that represents this case and implements &lt;code&gt;std::error::Error&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Again in weather.rs&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Display&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[derive(Debug)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;CouldNotFindLocation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;Display&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;CouldNotFindLocation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Formatter&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&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;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fmt&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;Result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nd"&gt;write!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Could not find location '{}'"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.place&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;CouldNotFindLocation&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now with all the types written, we create a new &lt;code&gt;async&lt;/code&gt; function that, given a place and a client, will return the forecast along with the location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Again in weather.rs&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Forecast&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;Box&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;dyn&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;error&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Endpoints we will use&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;LOCATION_REQUEST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://dataservice.accuweather.com/locations/v1/cities/search"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;DAY_REQUEST&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"http://dataservice.accuweather.com/forecasts/v1/daily/1day/"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// The URL to call combined with our API_KEY and the place (via the q search parameter)&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}?apikey={}&amp;amp;q={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LOCATION_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;place&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="c1"&gt;// Make the request we will call&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// Execute the request and await a JSON result that will be converted to a&lt;/span&gt;
        &lt;span class="c1"&gt;// vector of locations&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
        &lt;span class="nf"&gt;.execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="py"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Location&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Get the first location. If empty respond with the above declared&lt;/span&gt;
        &lt;span class="c1"&gt;// `CouldNotFindLocation` error type&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;first_location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;
        &lt;span class="nf"&gt;.into_iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.ok_or_else&lt;/span&gt;&lt;span class="p"&gt;(||&lt;/span&gt; &lt;span class="n"&gt;CouldNotFindLocation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;place&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;place&lt;/span&gt;&lt;span class="nf"&gt;.to_owned&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Now have the location combine the key/identifier with the URL &lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}{}?apikey={}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DAY_REQUEST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_location&lt;/span&gt;&lt;span class="py"&gt;.key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;forecast&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
        &lt;span class="nf"&gt;.execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
        &lt;span class="py"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Forecast&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="c1"&gt;// Combine the location with the foreact&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;first_location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forecast&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we have a function to get the weather, &lt;strong&gt;given a &lt;code&gt;reqwest&lt;/code&gt; client and a place&lt;/strong&gt;, we can wire that into the bots logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up the reqwest client
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;get_forecast&lt;/code&gt; requires a &lt;code&gt;reqwest&lt;/code&gt; Client and the weather API key. We will add some fields to our bot for holding this data and initialize this in the &lt;code&gt;shuttle_service::main&lt;/code&gt; function. Using the secrets feature we can get our weather API key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// In lib.rs&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;Bot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;weather_api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;reqwest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;discord_guild_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GuildId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[shuttle_service::main]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;serenity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;#[shared::Postgres]&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PgPool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nn"&gt;shuttle_service&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ShuttleSerenity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get the discord token set in `Secrets.toml` from the shared Postgres database&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;
        &lt;span class="nf"&gt;.get_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DISCORD_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CustomError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;weather_api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;
        &lt;span class="nf"&gt;.get_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"WEATHER_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CustomError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;discord_guild_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;
        &lt;span class="nf"&gt;.get_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"DISCORD_GUILD_ID"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CustomError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Set gateway intents, which decides what events the bot will be notified about&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;intents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;GatewayIntents&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;GUILD_MESSAGES&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;GatewayIntents&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;MESSAGE_CONTENT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;intents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.event_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Bot&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;weather_api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;reqwest&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                        &lt;span class="n"&gt;discord_guild_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;GuildId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discord_guild_id&lt;/span&gt;&lt;span class="nf"&gt;.parse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;.await&lt;/span&gt;
        &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Err creating client"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&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;
  
  
  Registering a /weather command
&lt;/h3&gt;

&lt;p&gt;We will add our new command with a place option/parameter. Back in the &lt;code&gt;ready&lt;/code&gt; hook, we can add an additional command alongside the existing &lt;code&gt;hello&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;commands&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;GuildId&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;set_application_commands&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;guild_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="py"&gt;.http&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="n"&gt;commands&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;commands&lt;/span&gt;
        &lt;span class="nf"&gt;.create_application_command&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="nf"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Say hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;.create_application_command&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;command&lt;/span&gt;
                &lt;span class="nf"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"weather"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Display the weather"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="nf"&gt;.create_option&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;option&lt;/span&gt;
                        &lt;span class="nf"&gt;.name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"place"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;.description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"City to lookup forecast"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;.kind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;CommandOptionType&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="nf"&gt;.required&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="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Discord allows us to set the expected type and whether it is required. Here, the place needs to be a string and is required.&lt;/p&gt;

&lt;p&gt;Now in the interaction handler, we can add a new branch to the match tree. We pull out the option/argument corresponding to &lt;code&gt;place&lt;/code&gt; and extract its value. Because of the restrictions made when setting the option we can assume that it is well-formed (unless Discord sends a bad request) and thus the unwraps here. After we have the arguments of the command we call the &lt;code&gt;get_forecast&lt;/code&gt; function and format the results into a string to return.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="s"&gt;"weather"&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;
        &lt;span class="py"&gt;.data&lt;/span&gt;
        &lt;span class="py"&gt;.options&lt;/span&gt;
        &lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.find&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="py"&gt;.name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"place"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.cloned&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argument&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="py"&gt;.value&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;place&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="nf"&gt;.as_str&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;weather&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get_forecast&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;place&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;        

    &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;forecast&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"Forecast: {} in {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;forecast&lt;/span&gt;&lt;span class="py"&gt;.headline.overview&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;location&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Err: {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running
&lt;/h3&gt;

&lt;p&gt;Now, we have these additional secrets we are using and we will add them to the &lt;code&gt;Secrets.toml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# In Secrets.toml&lt;/span&gt;
&lt;span class="c"&gt;# Existing secrets:&lt;/span&gt;
&lt;span class="py"&gt;DISCORD_TOKEN&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"***"&lt;/span&gt;
&lt;span class="py"&gt;DISCORD_GUILD_ID&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"***"&lt;/span&gt;
&lt;span class="c"&gt;# New secret&lt;/span&gt;
&lt;span class="py"&gt;WEATHER_API_KEY&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"***"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the secrets added, we can run the server:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cargo shuttle run&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;While typing, we should see our command come up with the options/parameters:&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%2Fn1ihhund6nlqbcmkiocj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fn1ihhund6nlqbcmkiocj.png" width="800" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Entering “Paris” as the place we get a result with a forecast:&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%2F6d0bc90ky8u2r2zr9rpq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6d0bc90ky8u2r2zr9rpq.png" width="800" height="333"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And entering a location that isn’t registered returns an error, thanks to the error handling we added to the &lt;code&gt;get_forecast&lt;/code&gt; function:&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%2Fk29c6r7p6vfczd3bmdhs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk29c6r7p6vfczd3bmdhs.png" width="597" height="207"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying on shuttle
&lt;/h3&gt;

&lt;p&gt;With all of that setup, it is really easy to get your bot hosted and running without having to run your PC 24/7.&lt;/p&gt;

&lt;p&gt;Just write:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cargo shuttle deploy&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;And you are good to go. Easy-pease, right?&lt;/p&gt;

&lt;p&gt;You could now take this idea even further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use a different API, to create a bot that can return &lt;a href="https://asli-fun-fact-api.herokuapp.com/" rel="noopener noreferrer"&gt;funny facts&lt;/a&gt; or return &lt;a href="https://spaceflightnewsapi.net/" rel="noopener noreferrer"&gt;new spaceflights&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Maybe you could use one of shuttle's provided databases to remember certain information about a user&lt;/li&gt;
&lt;li&gt;Expand on the weather forecast idea by adding more advanced options and follow-ups to command options&lt;/li&gt;
&lt;li&gt;Use the &lt;a href="https://discord.com/developers/docs/interactions/application-commands#localization" rel="noopener noreferrer"&gt;localization information&lt;/a&gt; to return information in other languages&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This blog post is powered by shuttle! If you have any questions, or want to provide feedback, join our &lt;a href="https://discord.gg/shuttle" rel="noopener noreferrer"&gt;Discord server&lt;/a&gt;!&lt;/p&gt;

&lt;p&gt;Shuttle: The Rust-native, open source, cloud development platform.&lt;br&gt;
Deploying and managing your Rust web apps can be an expensive, anxious and time consuming process.&lt;/p&gt;

&lt;p&gt;If you want a batteries included and ops-free experience, &lt;a href="https://github.com/shuttle-hq/shuttle" rel="noopener noreferrer"&gt;try out Shuttle&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
