<?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: Paul Works</title>
    <description>The latest articles on DEV Community by Paul Works (@paulworks).</description>
    <link>https://dev.to/paulworks</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3812869%2F6d52115f-134a-40c7-a90d-5e3cd9f64d60.png</url>
      <title>DEV Community: Paul Works</title>
      <link>https://dev.to/paulworks</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/paulworks"/>
    <language>en</language>
    <item>
      <title>Stop Hand-Holding Your AI: How to Build a Real-World Web Scraping Agent with Claude Tools 🕷️</title>
      <dc:creator>Paul Works</dc:creator>
      <pubDate>Sun, 26 Apr 2026 08:31:01 +0000</pubDate>
      <link>https://dev.to/paulworks/stop-hand-holding-your-ai-how-to-build-a-real-world-web-scraping-agent-with-claude-tools-267n</link>
      <guid>https://dev.to/paulworks/stop-hand-holding-your-ai-how-to-build-a-real-world-web-scraping-agent-with-claude-tools-267n</guid>
      <description>&lt;p&gt;Let’s be honest. Large Language Models (LLMs) are incredibly smart, but they suffer from one crippling weakness: &lt;strong&gt;they are trapped in a box.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you ask Claude to summarize a breaking news article or read documentation for a brand-new library released yesterday, it will apologize and say it doesn't have real-time internet access. &lt;/p&gt;

&lt;p&gt;But what if you could give Claude the ability to browse the web itself? &lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;Claude Tools&lt;/strong&gt; (Anthropic's version of Function Calling). By giving Claude tools, you transform it from a conversational chatbot into a powerhouse autonomous agent. &lt;/p&gt;

&lt;p&gt;Instead of showing you a boring "Get Current Time" example, we are going to build something you can actually use in production today: &lt;strong&gt;A Web-Scraping Assistant&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🎭 How Tool Use Actually Works
&lt;/h2&gt;

&lt;p&gt;Using tools with Claude isn't magic; it's a 3-step conversation loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You:&lt;/strong&gt; "Here is a prompt, and here is a list of Python scripts (&lt;code&gt;tools&lt;/code&gt;) you can ask me to run if you need help."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude:&lt;/strong&gt; "I need to read this URL. Please run the &lt;code&gt;fetch_webpage&lt;/code&gt; tool for me."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You:&lt;/strong&gt; &lt;em&gt;&lt;/em&gt; "Here is the raw text from the website!"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claude:&lt;/strong&gt; &lt;em&gt;&lt;/em&gt; "Here is the summary of the article..."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Claude &lt;strong&gt;never executes the code itself&lt;/strong&gt;. It just outputs a JSON payload telling &lt;em&gt;your&lt;/em&gt; server to run it. &lt;/p&gt;




&lt;h2&gt;
  
  
  💻 Let's Build It: Giving Claude the Internet
&lt;/h2&gt;

&lt;p&gt;First, let's write a real-world Python function using &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;BeautifulSoup&lt;/code&gt; to scrape any webpage and extract the text.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="c1"&gt;# 1. The actual Python function
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_webpage&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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&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;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Parse HTML and extract just the text
&lt;/span&gt;        &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&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="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;separator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Truncate to avoid blowing up the context window!
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; 
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to fetch webpage: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&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="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we tell Claude that this function exists by defining a &lt;strong&gt;JSON Schema&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 2. Tell Claude about it
&lt;/span&gt;&lt;span class="n"&gt;fetch_webpage_schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetch_webpage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fetches the raw text content of a given URL. Use this tool when you need to read an article, documentation, or any web page to answer the user&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s question.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The exact, fully qualified URL to scrape (e.g., https://example.com)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&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;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro-Tip 💡:&lt;/strong&gt; The &lt;code&gt;description&lt;/code&gt; field here is incredibly important. Claude reads it to decide &lt;em&gt;when&lt;/em&gt; and &lt;em&gt;whether&lt;/em&gt; it should use this tool! Outline exactly when the AI should reach for it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Wiring it up to the API
&lt;/h3&gt;

&lt;p&gt;Now let's ask Claude a question about recent news that it absolutely cannot answer without scraping the web.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;# Relies on ANTHROPIC_API_KEY in your .env
&lt;/span&gt;
&lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Can you read this article and give me 3 bullet points summarizing it? https://dev.to/about&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}]&lt;/span&gt;

&lt;span class="c1"&gt;# 3. Make the API Call
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fetch_webpage_schema&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# Pass the tool schema here!
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of returning a standard text message, Claude's &lt;code&gt;stop_reason&lt;/code&gt; will be &lt;code&gt;"tool_use"&lt;/code&gt;. It is asking &lt;em&gt;us&lt;/em&gt; to scrape the website for it!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Claude's response looks like this:
&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="nc"&gt;ToolUseBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;toolu_01...xyz&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;fetch_webpage&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
    &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;url&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://dev.to/about&lt;/span&gt;&lt;span class="sh"&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;
  
  
  Closing the Loop
&lt;/h3&gt;

&lt;p&gt;Claude is waiting for the data! It gave us the &lt;code&gt;ToolUseBlock&lt;/code&gt; telling us exactly what tool to execute, and grabbed the &lt;code&gt;url&lt;/code&gt; from our prompt. Let's finish the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A. Save Claude's tool request to our message history
&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# B. Actually execute the Python function locally
&lt;/span&gt;&lt;span class="n"&gt;tool_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;scraped_text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_webpage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;tool_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# C. Send the scraped text back to Claude
&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tool_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scraped_text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;is_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# D. Make the final API call so Claude can read the text and answer
&lt;/span&gt;&lt;span class="n"&gt;final_response&lt;/span&gt; &lt;span class="o"&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;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fetch_webpage_schema&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Output:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Based on the article at that URL, here are 3 key takeaways about DEV:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DEV is a community of software developers getting together to help one another out.&lt;/li&gt;
&lt;li&gt;The platform is built on open-source software called Forem.&lt;/li&gt;
&lt;li&gt;They focus on fostering an inclusive, decentralized, and positive environment for developers of all backgrounds."&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  🚀 Where to go from here?
&lt;/h2&gt;

&lt;p&gt;You've just built a fully functioning web-scraping AI Agent. But you don't have to stop at reading data. You can define tools that let Claude:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create calendar invites via the Google Calendar API&lt;/li&gt;
&lt;li&gt;Query your PostgreSQL database to generate custom SQL reports&lt;/li&gt;
&lt;li&gt;Execute Bash commands to manipulate files on your machine (just like Devin)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By bridging the gap between LLM reasoning and real-world execution, you are no longer building chatbots—you're building &lt;strong&gt;agents&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Take off the training wheels and give Claude a real tool today! &lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>claude</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Cut My Rails Hosting Costs by 70%: Migrating from Heroku to Railway</title>
      <dc:creator>Paul Works</dc:creator>
      <pubDate>Sun, 08 Mar 2026 14:59:09 +0000</pubDate>
      <link>https://dev.to/paulworks/i-cut-my-rails-hosting-costs-by-70-migrating-from-heroku-to-railway-22op</link>
      <guid>https://dev.to/paulworks/i-cut-my-rails-hosting-costs-by-70-migrating-from-heroku-to-railway-22op</guid>
      <description>&lt;h1&gt;
  
  
  I Cut My Rails Hosting Costs by 70%: Migrating from &lt;a href="https://www.heroku.com/" rel="noopener noreferrer"&gt;Heroku&lt;/a&gt; to &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;
&lt;/h1&gt;

&lt;p&gt;For a while, I was hosting a small Rails API on Heroku with JawsDB as the MySQL database.&lt;/p&gt;

&lt;p&gt;It worked perfectly. Heroku’s developer experience is still one of the best out there.&lt;/p&gt;

&lt;p&gt;But for a small personal project, I realized I was paying more than necessary.&lt;/p&gt;

&lt;p&gt;So I migrated the app and database to &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; and reduced my hosting costs from roughly &lt;strong&gt;~$20/month to ~$6/month&lt;/strong&gt; — about a &lt;strong&gt;70% reduction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The best part: the migration took less than &lt;strong&gt;30 minutes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here’s exactly how I did it.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I migrated a Rails API from &lt;strong&gt;Heroku + JawsDB&lt;/strong&gt; to &lt;strong&gt;Railway&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hosting cost reduced from &lt;strong&gt;~$20/month → ~$6/month&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Migration took &lt;strong&gt;~20–30 minutes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;No downtime&lt;/li&gt;
&lt;li&gt;Database migrated using &lt;strong&gt;one streaming command&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  My Original Setup
&lt;/h2&gt;

&lt;p&gt;My stack looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rails API
   │
Heroku Dyno
   │
JawsDB (MySQL)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Approximate monthly cost:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App hosting&lt;/td&gt;
&lt;td&gt;Heroku&lt;/td&gt;
&lt;td&gt;~$7–10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;JawsDB&lt;/td&gt;
&lt;td&gt;~$10–12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$20/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Again, not huge — but for side projects, I prefer keeping infrastructure simple and inexpensive.&lt;/p&gt;




&lt;h2&gt;
  
  
  The New Setup
&lt;/h2&gt;

&lt;p&gt;After migrating:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Rails API
   │
Railway App
   │
Railway MySQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New cost breakdown:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App hosting&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;~$3–4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MySQL&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;~$2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~$6/month&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That’s roughly a &lt;strong&gt;70% cost reduction&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Migration Strategy
&lt;/h2&gt;

&lt;p&gt;The migration consisted of four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Link the &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; project
&lt;/li&gt;
&lt;li&gt;Export the database from JawsDB
&lt;/li&gt;
&lt;li&gt;Import the data into &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; MySQL
&lt;/li&gt;
&lt;li&gt;Update the Rails configuration
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No downtime was required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Link Your Railway Project
&lt;/h2&gt;

&lt;p&gt;First install and authenticate with the &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; CLI.&lt;/p&gt;

&lt;p&gt;Then run:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;What this does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connects your local repo to your &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; project&lt;/li&gt;
&lt;li&gt;Allows the CLI to run commands against that project&lt;/li&gt;
&lt;li&gt;Creates a &lt;code&gt;.railway&lt;/code&gt; folder locally&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This step only needs to be done &lt;strong&gt;once per repository&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Dump Data from JawsDB and Import into Railway
&lt;/h2&gt;

&lt;p&gt;Instead of creating a dump file and importing it later, you can &lt;strong&gt;stream the database directly&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here’s the command I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mysqldump &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;jawsdb-host] &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;jawsdb-user] &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;jawsdb-password] &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--no-tablespaces&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--set-gtid-purged&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--single-transaction&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;jawsdb-database] &lt;span class="se"&gt;\&lt;/span&gt;
| railway run &lt;span class="nt"&gt;-s&lt;/span&gt; MySQL mysql &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nv"&gt;$MYSQLHOST&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nv"&gt;$MYSQLUSER&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="nv"&gt;$MYSQLPASSWORD&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;$MYSQLDATABASE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Exports the database from JawsDB
&lt;/li&gt;
&lt;li&gt;Pipes the output directly into &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; MySQL
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why These Flags Matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--no-tablespaces&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;JawsDB runs on AWS RDS, which restricts certain privileges required for dumping tablespaces.&lt;/p&gt;

&lt;p&gt;This flag avoids that issue.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--set-gtid-purged=OFF&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Prevents replication metadata from causing conflicts during import.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--single-transaction&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Creates a &lt;strong&gt;consistent snapshot&lt;/strong&gt; of the database while exporting.&lt;/p&gt;

&lt;p&gt;This works well for &lt;strong&gt;InnoDB tables&lt;/strong&gt;, which most Rails apps use.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Streaming the Database Is Powerful
&lt;/h2&gt;

&lt;p&gt;The pipe (&lt;code&gt;|&lt;/code&gt;) streams the SQL output directly into Railway.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;JawsDB → mysqldump → pipe → Railway MySQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No intermediate dump files&lt;/li&gt;
&lt;li&gt;Faster migration&lt;/li&gt;
&lt;li&gt;Lower disk usage&lt;/li&gt;
&lt;li&gt;Works well for large databases&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 3: Verify the Migration
&lt;/h2&gt;

&lt;p&gt;After the import, verify the migration with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;railway run &lt;span class="nt"&gt;-s&lt;/span&gt; MySQL mysql &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nv"&gt;$MYSQLHOST&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nv"&gt;$MYSQLUSER&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="nv"&gt;$MYSQLPASSWORD&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;$MYSQLDATABASE&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"SHOW TABLES;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the migration worked correctly, you should see your tables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;users
transactions
posts
comments
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Update Rails Configuration
&lt;/h2&gt;

&lt;p&gt;Previously, my Rails app used the JawsDB &lt;code&gt;DATABASE_URL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; automatically provides environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MYSQLHOST
MYSQLPORT
MYSQLUSER
MYSQLPASSWORD
MYSQLDATABASE
MYSQL_URL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The simplest Rails configuration is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;production&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;adapter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql2&lt;/span&gt;
  &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= ENV['MYSQL_URL'] %&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After pushing the code, &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; automatically redeployed the application.&lt;/p&gt;




&lt;h2&gt;
  
  
  Heroku vs Railway (Quick Comparison)
&lt;/h2&gt;

&lt;p&gt;Here’s a quick comparison based on my experience migrating a small Rails API.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Heroku + JawsDB&lt;/th&gt;
&lt;th&gt;Railway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hosting model&lt;/td&gt;
&lt;td&gt;App dynos + add-ons&lt;/td&gt;
&lt;td&gt;App + services in one platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;JawsDB MySQL add-on&lt;/td&gt;
&lt;td&gt;Native managed MySQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost (small project)&lt;/td&gt;
&lt;td&gt;~$20/month&lt;/td&gt;
&lt;td&gt;~$6/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Git push&lt;/td&gt;
&lt;td&gt;Git push / GitHub integration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment variables&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;td&gt;Supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI tools&lt;/td&gt;
&lt;td&gt;Heroku CLI&lt;/td&gt;
&lt;td&gt;Railway CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database migration&lt;/td&gt;
&lt;td&gt;Manual dump/import&lt;/td&gt;
&lt;td&gt;Easy via &lt;code&gt;railway run&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure management&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;td&gt;Managed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static outbound IP&lt;/td&gt;
&lt;td&gt;Not guaranteed&lt;/td&gt;
&lt;td&gt;Not guaranteed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;Mature production apps&lt;/td&gt;
&lt;td&gt;Side projects &amp;amp; small apps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Heroku still offers one of the best developer experiences.&lt;/p&gt;

&lt;p&gt;But for &lt;strong&gt;small projects or side tools&lt;/strong&gt;, simplifying infrastructure and reducing costs can make sense.&lt;/p&gt;

&lt;p&gt;For my Rails API, moving to &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simplified my stack&lt;/li&gt;
&lt;li&gt;reduced hosting costs by ~70%&lt;/li&gt;
&lt;li&gt;required minimal migration effort&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're running a &lt;strong&gt;small Rails project with MySQL on Heroku&lt;/strong&gt;, exploring &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt; might be worth considering.&lt;/p&gt;




&lt;h2&gt;
  
  
  Question for Other Developers
&lt;/h2&gt;

&lt;p&gt;If you're hosting side projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Are you still using Heroku?&lt;/li&gt;
&lt;li&gt;Or have you moved to platforms like &lt;a href="https://railway.com?referralCode=kZLahu" rel="noopener noreferrer"&gt;Railway&lt;/a&gt;, Fly.io, or Render?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love to hear what your stack looks like.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>devops</category>
      <category>mysql</category>
      <category>heroku</category>
    </item>
  </channel>
</rss>
