<?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: Garoro0920</title>
    <description>The latest articles on DEV Community by Garoro0920 (@garoro0920).</description>
    <link>https://dev.to/garoro0920</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%2F3950874%2F3e47f827-dcf9-4033-920c-d72a6b71db32.png</url>
      <title>DEV Community: Garoro0920</title>
      <link>https://dev.to/garoro0920</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/garoro0920"/>
    <language>en</language>
    <item>
      <title>I built an AI agent that migrates Next.js Pages Router to App Router</title>
      <dc:creator>Garoro0920</dc:creator>
      <pubDate>Mon, 25 May 2026 14:36:24 +0000</pubDate>
      <link>https://dev.to/garoro0920/i-built-an-ai-agent-that-migrates-nextjs-pages-router-to-app-router-5747</link>
      <guid>https://dev.to/garoro0920/i-built-an-ai-agent-that-migrates-nextjs-pages-router-to-app-router-5747</guid>
      <description>&lt;p&gt;Most Next.js teams have a Pages Router → App Router migration sitting in their backlog. It's mechanical but careful work, and it keeps getting deprioritized. I built &lt;a href="https://migrate-bot.dev" rel="noopener noreferrer"&gt;migrate-bot&lt;/a&gt; to automate it end-to-end, and this post is about how it works under the hood.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the migration actually involves
&lt;/h2&gt;

&lt;p&gt;App Router isn't just "move files to a new folder". The semantic changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;getStaticProps&lt;/code&gt; / &lt;code&gt;getServerSideProps&lt;/code&gt; → &lt;code&gt;async&lt;/code&gt; Server Components&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;getStaticPaths&lt;/code&gt; → &lt;code&gt;generateStaticParams&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next/router&lt;/code&gt; → &lt;code&gt;next/navigation&lt;/code&gt; (&lt;code&gt;useRouter&lt;/code&gt; / &lt;code&gt;usePathname&lt;/code&gt; / &lt;code&gt;useSearchParams&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;next/head&lt;/code&gt; → the Metadata API (&lt;code&gt;export const metadata&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pages/_app.tsx&lt;/code&gt; + &lt;code&gt;pages/_document.tsx&lt;/code&gt; → &lt;code&gt;app/layout.tsx&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API routes: &lt;code&gt;pages/api/x.ts&lt;/code&gt; (single handler) → &lt;code&gt;app/api/x/route.ts&lt;/code&gt; (&lt;code&gt;export async function GET/POST/...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;pages/&lt;/code&gt; → &lt;code&gt;app/&lt;/code&gt; restructure, including &lt;code&gt;[slug]&lt;/code&gt; dynamic routes&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The whole thing runs serverless + ephemeral:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Workers&lt;/strong&gt; host the API and landing site&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare D1&lt;/strong&gt; (SQLite) stores jobs, orders, installations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Queues&lt;/strong&gt; decouple the webhook handler from the migration pipeline (producer/consumer with an idempotency check so retries don't double-charge)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fly.io Machines&lt;/strong&gt; run the actual migration: one ephemeral VM per job, destroyed at job end (no code retention)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anthropic Claude&lt;/strong&gt; does the per-file transforms (Sonnet by default, Opus as a one-shot retry fallback)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe&lt;/strong&gt; for payment, &lt;strong&gt;Resend&lt;/strong&gt; for transactional email&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The pipeline as a state machine
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;queued → analyzing → planning → migrating → verifying → pr_ready
                                                ↓ (on failure)
                          aborted_blocker → refunding → refunded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each transition is persisted to D1 so every job is observable, and a mid-pipeline crash transitions to &lt;code&gt;aborted_blocker&lt;/code&gt; → automatic refund rather than leaving the customer charged for nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A subtle bug: _document.tsx
&lt;/h2&gt;

&lt;p&gt;Early on, every repo with a &lt;code&gt;pages/_document.tsx&lt;/code&gt; failed. The planner maps both &lt;code&gt;_app.tsx&lt;/code&gt; and &lt;code&gt;_document.tsx&lt;/code&gt; to the same target (&lt;code&gt;app/layout.tsx&lt;/code&gt;), and my migrate step recorded the second one as a "failed task" when it detected the target was already written. The pipeline treated any failed task as fatal — so the whole migration aborted.&lt;/p&gt;

&lt;p&gt;The fix was to distinguish &lt;em&gt;intentional skips&lt;/em&gt; (planned target collisions) from &lt;em&gt;real failures&lt;/em&gt; (the LLM aborting or erroring). I added a &lt;code&gt;skippedTaskIds&lt;/code&gt; field separate from &lt;code&gt;failedTaskIds&lt;/code&gt;, and surfaced the skip in the PR description so the customer knows to manually merge any custom &lt;code&gt;&amp;lt;Html&amp;gt;&lt;/code&gt; attributes from their &lt;code&gt;_document.tsx&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trust model
&lt;/h2&gt;

&lt;p&gt;Since this rewrites real production code, the trust mechanisms matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The output is always a &lt;strong&gt;draft PR&lt;/strong&gt; — your CI runs first, you review, you merge. Nothing is auto-merged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;14-day full refund&lt;/strong&gt; if the verify step (typecheck + &lt;code&gt;next build&lt;/code&gt;) can't pass for reasons attributable to the service.&lt;/li&gt;
&lt;li&gt;Code is processed only on an isolated VM and is never used to train any model.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it / feedback
&lt;/h2&gt;

&lt;p&gt;It's live at &lt;a href="https://migrate-bot.dev" rel="noopener noreferrer"&gt;migrate-bot.dev&lt;/a&gt;. Pricing is per-repo by file count ($99 / $249 / $499). I'd genuinely value feedback on the approach — especially the pricing shape and the draft-PR + refund trust model.&lt;/p&gt;

&lt;p&gt;(Currently not offered to EEA/UK/Switzerland residents due to GDPR territorial scope; revisiting post-launch.)&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>react</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
