<?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: Alex Wilson</title>
    <description>The latest articles on DEV Community by Alex Wilson (@alexwilson).</description>
    <link>https://dev.to/alexwilson</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%2F225432%2Fe9084127-1312-497f-9a71-2219a991e082.jpeg</url>
      <title>DEV Community: Alex Wilson</title>
      <link>https://dev.to/alexwilson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexwilson"/>
    <language>en</language>
    <item>
      <title>Migrating to the Next.js App Router (or: how I learned to stop worrying and love Server Actions)</title>
      <dc:creator>Alex Wilson</dc:creator>
      <pubDate>Fri, 12 Dec 2025 05:25:51 +0000</pubDate>
      <link>https://dev.to/alexwilson/migrating-to-the-nextjs-app-router-or-how-i-learned-to-stop-worrying-and-love-server-actions-m2e</link>
      <guid>https://dev.to/alexwilson/migrating-to-the-nextjs-app-router-or-how-i-learned-to-stop-worrying-and-love-server-actions-m2e</guid>
      <description>&lt;p&gt;Hi, I’m &lt;a href="https://alexwilson.tech" rel="noopener noreferrer"&gt;Alex Wilson&lt;/a&gt; and I’m a Staff Engineer in the Business Platform Development Division of Money Forward.  Welcome to day 5 of our &lt;a href="https://adventar.org/calendars/11946" rel="noopener noreferrer"&gt;2025 advent calendar&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In 2025, we overhauled one of our customer-facing products to make it faster, more secure and more reliable.&lt;/p&gt;

&lt;p&gt;We achieved this by consolidating business logic to run on the server, server-rendering with React Server Components, and unlocking both of these things by migrating to the Next.js App Router.&lt;/p&gt;

&lt;p&gt;In this post I want to talk about how we did this, and about the challenges we faced along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 1. The Challenge
&lt;/h2&gt;

&lt;p&gt;We build many web applications with Next.js, which is a framework for building full-stack React applications.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next.js and its routers
&lt;/h3&gt;

&lt;p&gt;Before Next.js version 13, its primary architecture was called the Pages Router, which would detect React Components in a &lt;code&gt;src/pages&lt;/code&gt; directory, allow them to do some data-fetching and then compile these into the actual pages/views shown to users.  For most applications, this means that the work to show UI happens both on the server and then again in the browser.&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%2Fpkp920u25ncj0wh1l174.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%2Fpkp920u25ncj0wh1l174.png" alt=" " width="800" height="259"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With version 13, in partnership with the React team, Next.js introduced a new architecture called the App Router, which changed many things but enabled two new major features in React: React Server Components and React Server Actions.  React Server Components are React components which only execute on the server.  Optionally, in this architecture, existing React components can now request to only be rendered on the client.&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%2F3z30xioxe8uhej4k7rwf.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%2F3z30xioxe8uhej4k7rwf.png" alt=" " width="800" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At time of writing, Next.js have not announced an official schedule for the Pages Router to be deprecated, but many new features introduced since 2023 only target/support the App Router.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why migrate?
&lt;/h3&gt;

&lt;p&gt;Aside from wanting to align with the long-term technical directions of both React and Next.js, we anticipated these benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Improved user experience&lt;/strong&gt;: Moving work such as data-fetching and complex validation server-side means that users encounter fewer unhandled error scenarios. React also includes features like Suspense which give us the opportunity to add animations and transitions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Improved code quality&lt;/strong&gt;: A clearer separation between data-fetching/validation and UI code means that we can re-use more of it, and test it much more thoroughly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster page-loads&lt;/strong&gt;: The changes result in smaller, faster and snappier web-pages and web-apps &lt;a href="https://wpostats.com/" rel="noopener noreferrer"&gt;leading to increased engagement, retention and conversion&lt;/a&gt; which are always welcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  OK, but why did &lt;em&gt;you&lt;/em&gt; migrate now?
&lt;/h3&gt;

&lt;p&gt;OK, so the benefits &lt;em&gt;sound&lt;/em&gt; good, but if there’s no deadline, why migrate at all?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In short: It wasn’t just about the App Router, so if you’re here only for that, you can skip ahead to Chapter 2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On top of the benefits above, we thought that migrating to the App Router was a golden opportunity to revisit our frontend architecture. We would also learn what a migration to the App Router actually involves for our other products when a Pages Router deprecation is scheduled.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our starting position
&lt;/h2&gt;

&lt;p&gt;Many of our smaller applications are built using the Pages Router, and are thin UI layers on top of robust backend APIs.  This approach has allowed originally more backend-heavy teams to quickly build out rich products, however has drawbacks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Maintenance costs compound&lt;/strong&gt;: These backend APIs are tightly coupled to frontends, and as product functionality expands so too does the time required to launch &amp;amp; support features.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User experience options are limited&lt;/strong&gt;: This approach means that logic usually runs client-side, but that it’s &lt;em&gt;sometimes&lt;/em&gt; optimistically run server-side, and this inconsistency translates directly to the user experience.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And so, anticipating more UI-heavy feature development in one of our products, we thought that this was the right time to migrate it and overhaul it in the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 2: The Plan
&lt;/h2&gt;

&lt;p&gt;Before anything else, we followed &lt;a href="https://nextjs.org/docs/app/guides/migrating/app-router-migration" rel="noopener noreferrer"&gt;the official migration guide&lt;/a&gt; to do a (very) basic proof-of-concept to see where the edges would be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Proving the concept
&lt;/h3&gt;

&lt;p&gt;In a temporary branch, we deleted all but one of our pages, and renamed the &lt;code&gt;src/pages&lt;/code&gt; directory to &lt;code&gt;src/app&lt;/code&gt; (step 1), copied in a blank &lt;code&gt;layout&lt;/code&gt; (step 2) and extracted our client-only state-management (Providers).  From here we removed functionality and customizations one-by-one until we were able to compile, which gave us which areas we needed to focus on:&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 1: There was a problem with our configuration
&lt;/h3&gt;

&lt;p&gt;Our code-base collocated pages, components and tests by leveraging the custom file-extensions parameter.  We removed our tests and separated its components which let our application compile!&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 2: None of our error handling worked
&lt;/h3&gt;

&lt;p&gt;In the App Router, error handling changed significantly, with “Not Found” being executed as a catch-all route server-side, and all other errors moving to an Error Boundary which is caught client-side.&lt;/p&gt;

&lt;p&gt;Our error-handling code had been built to execute on both server-and-client, so we temporarily removed all of the server-side error handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 3: Some of our UI dependencies wouldn’t server-render
&lt;/h3&gt;

&lt;p&gt;Some third-party dependencies made use of features that only worked on the client but didn’t specifically opt-out of server-rendering.  To solve this, we followed the same pattern we’d used for client-only state management: Split some of our React components into server-only components to act as a “shell”, and inner client-only components.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import LikeButton from "./LikeButton";

export default function Page() {
  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h1&amp;gt;Post title&amp;lt;/h1&amp;gt;
      &amp;lt;LikeButton /&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";
import { useState } from "react";

export default function LikeButton() {
  const [liked, set] = useState(false);
  return (
    &amp;lt;button onClick={() =&amp;gt; set(!liked)}&amp;gt;
      {liked ? "❤️" : "♡"}
    &amp;lt;/button&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By making these changes, we were able to compile, log-in and perform some basic operations!  Great, so all we need to do is solve these things, and it’ll all be fine!  Famous last words.&lt;/p&gt;

&lt;p&gt;All that was missing from our plan was a release strategy.  From the pilot, it was clear that the migration would take some time.  To minimize risk and disruption to users, we couldn’t do this as a big-bang release, and we also couldn’t suspend routine feature development &amp;amp; bug fixing. This meant that a period of dual-running was unavoidable.&lt;/p&gt;

&lt;p&gt;And so we formed a four-step plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Enable the App-Router&lt;/strong&gt;: Let it take over all error-handling, but without it handling any other user-facing functionality.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release one page&lt;/strong&gt;: Find a candidate new feature, build it in the App Router and release it as normal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progressively migrate other pages&lt;/strong&gt;: Integrate feature-flagging, duplicate pages and port them and their shared dependencies and release them one-by-one behind a feature-flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the Pages Router completely&lt;/strong&gt;: Remove remnants of the Pages Router, and revert all changes we make to enable dual-running.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Great. Let’s go!&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 3: Switching on the App-Router
&lt;/h2&gt;

&lt;p&gt;Ultimately this proved to be the most time-consuming step, because all we did in the proof-of-concept step was avoid solving these first two problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Changing our Page Extensions
&lt;/h3&gt;

&lt;p&gt;Problem 1 was that our existing folder structure wouldn’t reliably compile, and when it did compile, all pages would throw an error.&lt;/p&gt;

&lt;p&gt;To explain this, we need to look at what a stock-configuration Pages Router page looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/pages/[test].tsx
src/test/pages/[test].test.ts
src/components/[test]/test-component.tsx
src/test/components/[test]/test-component.test.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;[test]&lt;/code&gt; is the actual path used for the page, and this pattern is baked into the filename of the page&lt;/p&gt;

&lt;p&gt;This is OK at first, but as the number of pages, components and their test cases grow, it can become quite difficult to keep track of everything, and so many people collocate components together.&lt;/p&gt;

&lt;p&gt;In the Pages Router, you can use the &lt;code&gt;pageExtensions&lt;/code&gt; configuration parameter to enable this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// next.config.js&lt;/span&gt;
&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;pageExtensions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page.tsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;page.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then our folder structure looks more like this, where &lt;code&gt;.page.tsx&lt;/code&gt; is what NextJS is looking for, and the route is still &lt;code&gt;/[test]&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/pages/[test].page.tsx
src/pages/[test].test.ts
src/pages/[test]/components/test-component.tsx
src/pages/[test]/components/test-component.test.ts
src/pages/[test]/components/test-component.stories.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Luckily, collocation has become the norm, and the equivalent in the App Router looks 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;src/pages/[test]/page.tsx
src/pages/[test]/page.test.ts
src/pages/[test]/components/test-component.tsx
src/pages/[test]/components/test-component.test.ts
src/pages/[test]/components/test-component.stories.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It should be as simple as keeping the &lt;code&gt;.page.tsx&lt;/code&gt; extension, right?&lt;br&gt;
Unfortunately, no.  At time of writing, the &lt;code&gt;pageExtensions&lt;/code&gt; parameter is effectively unsupported in the App Router (&lt;a href="https://github.com/vercel/next.js/issues/23959" rel="noopener noreferrer"&gt;Next.js #23959&lt;/a&gt;, &lt;a href="https://github.com/vercel/next.js/issues/65447" rel="noopener noreferrer"&gt;#65447&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Aside from a few of the solutions in related GitHub issues, here’s what we tried:&lt;br&gt;
❌ Using &lt;code&gt;.page.tsx&lt;/code&gt; for both - App Router build fails&lt;br&gt;
❌ Using &lt;code&gt;.tsx&lt;/code&gt; for both - Page Router picks up test/story files as routes&lt;br&gt;
❌ Separating all collocated files - Massive refactor that would be too disruptive for ongoing work for our period of dual-running&lt;/p&gt;
&lt;h4&gt;
  
  
  Why not both?
&lt;/h4&gt;

&lt;p&gt;A throwaway comment of, “Why not try everything at the same time?” went a bit too far, and we realized that we could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bulk rename &lt;code&gt;src/pages&lt;/code&gt; to something like &lt;code&gt;src/pagerouter_pages&lt;/code&gt;, which is one change in Git and won’t break anyone’s workflows. &lt;/li&gt;
&lt;li&gt;Create proxies for all of the pages in the &lt;code&gt;src/pagerouter_pages&lt;/code&gt; directory, in a new &lt;code&gt;src/pages&lt;/code&gt; directory. And by automating this, we could hook it into our existing NPM workflow.&lt;/li&gt;
&lt;li&gt;Remove the custom &lt;code&gt;pageExtensions&lt;/code&gt; configuration completely, allowing the App Router to compile, and for the application to load.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And so that’s what we did.  We wrote a script which automatically generated proxy classes which re-exported all &lt;code&gt;.page.tsx&lt;/code&gt; exports from the freshly renamed &lt;code&gt;pagerouter_pages&lt;/code&gt; directory.&lt;/p&gt;

&lt;p&gt;The source-code for this script is available on this Gist:&lt;br&gt;
&lt;a href="https://gist.github.com/alexwilson/b1e4fa1eb0017c67132c25eb1c134e5e" rel="noopener noreferrer"&gt;alexwilson/b1e4fa1eb0017c67132c25eb1c134e5e&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But hopefully this will be fixed soon.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rebuilding Error-Handling
&lt;/h3&gt;

&lt;p&gt;Problem 2 was that our existing error-handling did not work.&lt;/p&gt;

&lt;p&gt;We started with a typical JavaScript-style error handling: Try/Catch blocks on individual operations, and a top-level catch which would attempt to recover uncaught errors, show a toast to users and then finally log them.&lt;/p&gt;

&lt;p&gt;What we needed (at minimum) was: A global &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/error" rel="noopener noreferrer"&gt;Error Boundary&lt;/a&gt; and a &lt;a href="https://nextjs.org/docs/app/api-reference/file-conventions/not-found" rel="noopener noreferrer"&gt;“Not Found”&lt;/a&gt; page.  Creating these was quite easy, we repeated what we’d done in the proof-of-concept.&lt;/p&gt;

&lt;p&gt;But we still needed server-side error handling.&lt;/p&gt;

&lt;p&gt;Luckily all our existing API calls were made using the Axios library, which supports the concept of &lt;a href="https://axios-http.com/docs/interceptors" rel="noopener noreferrer"&gt;Interceptors&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Interceptors allow us to catch errors returned by API calls, and so we started by mirroring our previous top-level server-side error handling.&lt;/p&gt;

&lt;p&gt;For local and more specific error-handling scenarios, we chose to handle them whilst migrating related functionality.&lt;/p&gt;
&lt;h3&gt;
  
  
  Run-Time Configuration
&lt;/h3&gt;

&lt;p&gt;Something we missed in the proof-of-concept is that while Next.js’s Pages Router supports setting/changing application configuration at run-time, the App Router does not.&lt;/p&gt;

&lt;p&gt;This makes perfect sense: If you’re statically compiling JavaScript for the browser in a CI environment, it will only have access to the variables available in that environment, even if we are using different information in production.&lt;/p&gt;

&lt;p&gt;We used this Pages Router pattern a lot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;getConfig&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;publicRuntimeConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;publicRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Some&lt;/span&gt; &lt;span class="nx"&gt;Link&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/a&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/header&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We build &amp;amp; artefact all of our systems in a CI environment before deploying them to a cluster where we set run-time variables &lt;em&gt;all the time&lt;/em&gt;, and so not being able to do this was a deal-breaker.&lt;/p&gt;

&lt;p&gt;We needed a solution that would work for both Page and App Router.  Theoretically all it needed to do was conditionally read some runtime state from the server when in the App Router context, and for Page Router pages fall-back to the Pages Router the existing &lt;code&gt;publicRuntimeConfig&lt;/code&gt; API.&lt;/p&gt;

&lt;p&gt;We found a nice package called &lt;a href="https://github.com/expatfile/next-runtime-env" rel="noopener noreferrer"&gt;next-runtime-env&lt;/a&gt; which met our security standards and which would handle transmitting the state in App Router.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;NOTE: At time of writing, this package doesn’t support all versions of Next.js 15, or Next.js 16.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;With the help of this package, we wrote a small internal abstraction layer which surfaced an almost identical API to &lt;code&gt;publicRuntimeConfig&lt;/code&gt; which made rolling it out quite easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { env as appRouterEnv } from 'next-runtime-env';

  // Map of config property name to environment variable name
  export const PUBLIC_RUNTIME_CONFIG_MAP: Record&amp;lt;string, string&amp;gt; = {
    baseUrl: 'NEXT_PUBLIC_SOME_URL',
  };

  export function getPublicRuntimeConfig() {
    const config: Record&amp;lt;string, string | undefined&amp;gt; = {};

    // Populate object matching original publicRuntimeConfig
    for (const [key, envVar] of Object.entries(PUBLIC_RUNTIME_CONFIG_MAP)) {
      config[key] = appRouterEnv(envVar) || undefined;
    }

    return config;
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPublicRuntimeConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@lib/publicRuntimeConfig&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;publicRuntimeConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPublicRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;publicRuntimeConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="nx"&gt;Some&lt;/span&gt; &lt;span class="nx"&gt;Link&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/a&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/header&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Switching it on
&lt;/h3&gt;

&lt;p&gt;Phew.  That was a lot.  But it seems to be working fine now!&lt;/p&gt;

&lt;p&gt;However, since we’ve now touched a lot of functionality (including &lt;em&gt;all error handling&lt;/em&gt;), our next step was to check for any new bugs and errors by running a &lt;a href="https://moneyforward-dev.jp/entry/2019/12/17/bugbash-to-qa/" rel="noopener noreferrer"&gt;Bug Bash&lt;/a&gt;.  No major issues.&lt;/p&gt;

&lt;p&gt;And, after all that, finally, we switched the App Router on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 4: Migrating our first page
&lt;/h2&gt;

&lt;p&gt;Now that the App Router is running, we can begin the “migration” part of the migration.  Here we’re taking an in-development feature adding a new page, and working with the responsible team to build it directly into the App Router.&lt;/p&gt;

&lt;p&gt;This should be a win for everybody, because App Router pages require far less boilerplate than their Pages Router counterparts.  But what’s missing?&lt;/p&gt;

&lt;h3&gt;
  
  
  Supporting shared components in both routers
&lt;/h3&gt;

&lt;p&gt;This proved to be &lt;em&gt;relatively&lt;/em&gt; straightforward?&lt;/p&gt;

&lt;p&gt;Originally we saw an issue with our dependencies where they would attempt to use client-only features on the server, but during the real thing we narrowed this down to only one package.&lt;/p&gt;

&lt;p&gt;We mitigated this issue almost completely by finding references to this dependency and forming new boundaries between server &amp;amp; client components around it.&lt;/p&gt;

&lt;p&gt;We did this almost identically to during our pilot, by adding splitting components and adding &lt;code&gt;"use client";&lt;/code&gt; directives to the ones directly including this dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication &amp;amp; Middleware
&lt;/h3&gt;

&lt;p&gt;The next issue was auth.&lt;/p&gt;

&lt;p&gt;A common pattern in single-page-applications is to assume users are signed in and have access to stuff, and to then catch 400/403 errors in API calls and modify the UI based on this.  This is a gross simplification, of course, but initially we did something like this by catching errors thrown on per-API-call level.&lt;/p&gt;

&lt;p&gt;However now that we’re handling errors differently, this approach doesn’t work (and has become inexplicably much slower?)&lt;/p&gt;

&lt;p&gt;Here, &lt;a href="https://nextjs.org/docs/app/guides/authentication" rel="noopener noreferrer"&gt;Next.js’s recommendation is to use middleware&lt;/a&gt;.  All of our products already use some form of session, so we can implement this in a stateless way.&lt;/p&gt;

&lt;p&gt;However, Next.js 12 deprecated structured/staggered Middleware, and has instead moved Middleware to a single file.  We were looking to structure our middleware to separate concerns like auth.  This is where &lt;a href="https://nemo.zanreal.com/" rel="noopener noreferrer"&gt;Next.js Easy Middleware (NEMO)&lt;/a&gt; came in.&lt;/p&gt;

&lt;p&gt;We built a small auth middleware that attempted to validate a session, and if it was invalid, would send users to their login page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Metadata API
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://nextjs.org/docs/app/getting-started/metadata-and-og-images" rel="noopener noreferrer"&gt;Metadata API&lt;/a&gt; replaces previous usage of the &lt;code&gt;next/head&lt;/code&gt; component, and honestly this was very simple to adopt.  It’s fewer lines of code and far more predictable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Our first App Router page
&lt;/h3&gt;

&lt;p&gt;A feature-team was building new functionality which added a new page (instead of modifying any existing ones), and so we partnered to build it as an App Router page.  This meant less boilerplate.  It went through testing as normal, found no issues, and released without issue.&lt;/p&gt;

&lt;p&gt;Great, now we have an App Router page in production!&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 5: Migrating everything else
&lt;/h2&gt;

&lt;p&gt;Now for the real challenge: Migrating everything else.&lt;/p&gt;

&lt;p&gt;At this point, both the App Router and Pages Router are serving traffic, with the App Router handling global error handling, and auth logic happening in middleware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature Flagging
&lt;/h3&gt;

&lt;p&gt;We had already integrated a feature-flagging library called &lt;a href="https://github.com/flipt-io/flipt-client-sdks/tree/main/flipt-client-js" rel="noopener noreferrer"&gt;Flipt&lt;/a&gt; which would segment users based on their session.  &lt;/p&gt;

&lt;p&gt;To port pages to the App Router, we copied them and ported their boilerplate to the App Router layout, and put them under a new prefix.&lt;/p&gt;

&lt;p&gt;However, how do we actually send users to this new page?  We built a new middleware, chained to execute after auth, which would evaluate the feature-flag and perform an internal rewrite, and it looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/middleware/page-to-app-migration.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROUTER_MAPPING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/..app-router/page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="c1"&gt;// ... more mappings&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;middleware&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextRequest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Check if this path has an App Router version&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ROUTER_MAPPING&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getFeatureFlag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pageToAppMigration&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROUTER_MAPPING&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="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&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="c1"&gt;// Otherwise, continue to Page Router&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  next/router/compat
&lt;/h3&gt;

&lt;p&gt;How do components (for example, pagination?) push navigation changes?&lt;/p&gt;

&lt;p&gt;One crucial change between Pages Router and App Router is a move from the &lt;code&gt;next/router&lt;/code&gt; hook in the Pages Router to &lt;code&gt;next/navigation&lt;/code&gt; hook in the App Router.  They offer similar functionality, but since they are dependent on the router, they are incompatible with one another and importing the wrong one throws an irrecoverable error.&lt;/p&gt;

&lt;p&gt;Next provides a &lt;code&gt;next/router/compat&lt;/code&gt; module &lt;a href="https://nextjs.org/docs/pages/api-reference/functions/use-router#the-nextcompatrouter-export" rel="noopener noreferrer"&gt;which can be imported into App Router pages&lt;/a&gt;, however, when used in the App Router it doesn’t do anything: All &lt;code&gt;compat&lt;/code&gt; does here is return &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Similarly, the &lt;code&gt;next/router&lt;/code&gt; supported “shallow” pushes which is when the client adjusts the URL without performing a full server-driven re-render.  We use this pattern a lot in areas like search when the user changes something but we don’t need to perform a full re-render.&lt;/p&gt;

&lt;p&gt;So to make components compatible with both routers, and to keep shallow pushes, we wrote a new hook which uses both routers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// hooks/useRouterPush.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useRouter&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;useNextRouter&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;next/compat/router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;QueryString&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;qs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;useRouterPush&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageRouter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useNextRouter&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;push&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Partial&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;shallow&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;href&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;QueryString&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;arrayFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brackets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Try Page Router first&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pageRouter&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;pageRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;pageRouter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;shallow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;shallow&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="c1"&gt;// Fall back to Web History API for App Router&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pushState&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`?&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;push&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;
  
  
  From Client Components to Server Actions
&lt;/h3&gt;

&lt;p&gt;Some functionality which had been client-based saw a major slowdown when we moved to the App Router, even when we attempted to opt-out of Server Components by using the &lt;code&gt;”use client”;&lt;/code&gt; directive.&lt;/p&gt;

&lt;p&gt;We weren’t able to fully explain this slowdown, but we believed it was related to preloading.  So to address this, we ported their functionality to Server Actions.  &lt;/p&gt;

&lt;p&gt;This resulted in the double-whammy of a reduction in boilerplate (and easier testing), as well as even faster performance than pre-migration:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Form-based mutations with Server Actions&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;serverAction&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Server-side redirects after mutations&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;use server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;something&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another nice benefit is that these are progressively enhanced by default meaning that if client-side JavaScript fails for some reason this functionality will now continue to work!&lt;/p&gt;

&lt;h3&gt;
  
  
  Release train
&lt;/h3&gt;

&lt;p&gt;From this point, we followed the above steps to fully migrate all remaining components and pages, to test them, and then launch them one-by-one.&lt;/p&gt;

&lt;p&gt;This would not have been possible without feature-flagging, which allowed us to switch on &amp;amp; off the new App Router pages.  We began by targeting them at our internal tenant first, and then rolling out to users sequentially.&lt;/p&gt;

&lt;p&gt;After App Router pages seemed stable, we began graduating them by removing them from the mapping table in our middleware, and deleting their Page Router equivalents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 6: We’re on the App Router
&lt;/h2&gt;

&lt;p&gt;OK!  Now we have all of our pages running in the App Router, and we’ve deleted all of our Pages Router pages.&lt;/p&gt;

&lt;p&gt;We now have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A routing compat hook&lt;/li&gt;
&lt;li&gt;Feature-flagging middleware&lt;/li&gt;
&lt;li&gt;The original Pages Router layout&lt;/li&gt;
&lt;li&gt;A workflow which attempts to proxy Page Router pages&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Taking advantage of the App Router
&lt;/h3&gt;

&lt;p&gt;One of the major benefits of the App Router was letting us revisit our business logic, and so we took full advantage of this for both server &amp;amp; client rendering.&lt;/p&gt;

&lt;p&gt;We introduced a “domain layer” which sits between Pages and our backend APIs, allowing us to separate our views and our data with versions of Model-View-ViewModel &amp;amp; Repository patterns.&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%2F2g8kapuv4vo5w1e62iw8.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%2F2g8kapuv4vo5w1e62iw8.png" alt=" " width="800" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Meaning that our business logic now looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;View (React Components)&lt;/li&gt;
&lt;li&gt;Model &amp;amp; View Model (Business logic in entities/*/index.ts)&lt;/li&gt;
&lt;li&gt;Repository (Mappers in entities/*/mapper.ts)&lt;/li&gt;
&lt;li&gt;Data sources (API clients)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Caching
&lt;/h3&gt;

&lt;p&gt;This application is fully user-dynamic, so we did not change or modify the caching strategy during the migration.  This is an area for future improvement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deleting all of the things
&lt;/h3&gt;

&lt;p&gt;We chose to keep our routing hook, because it was a useful abstraction in general.  So we added more testing around it and have kept it.  It would be useful to see shallow-routing functionality in future.&lt;/p&gt;

&lt;p&gt;But other than this, we no longer need any of the features/changes we made to support dual-running the App Router and Pages Router.  This made for a very satisfying series of commits which were (mostly) deleting code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chapter 7: Conclusion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What did we get?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;We’re now on the App Router, and we were able to do it in our own time.&lt;/li&gt;
&lt;li&gt;Our client-bundles are smaller, and some core journeys (esp. sign-in) are significantly faster.&lt;/li&gt;
&lt;li&gt;Moving more logic to Middleware has allowed us to centralize core functionality and reduce boilerplate across the board.&lt;/li&gt;
&lt;li&gt;Server Actions have let us separate concerns between view logic and business logic, and to introduce an abstraction that makes it easier to develop frontend &amp;amp; backend independently.&lt;/li&gt;
&lt;li&gt;Better automated testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What went well?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Feature-flagging helped us reduce risk as we migrated from one router to the other.&lt;/li&gt;
&lt;li&gt;TypeScript came in clutch constantly during this project. All of the subtle deprecations and changes were flagged immediately, which combined with enforcing strict typing (including now banning use of the &lt;code&gt;any&lt;/code&gt; keyword!) let us completely avoid (probably) &lt;em&gt;thousands&lt;/em&gt; of bugs.&lt;/li&gt;
&lt;li&gt;Dual-running went well, and since we were able to avoid any major workflow changes we didn’t suffer any major slowdown of feature-development.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What didn’t go so well?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It took ages&lt;/strong&gt;: If we had done a “big-bang” migration, it would have been much faster: Dual-running and avoiding disrupting ongoing development added a lot of complexity to the actual migration which already had its own challenges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Migration documentation is lacking&lt;/strong&gt;: Behavioral changes like pageExtensions, error-handling and &lt;code&gt;next/navigation&lt;/code&gt; not fully supporting all &lt;code&gt;next/router&lt;/code&gt; features are things we discovered during development but would have liked to know up-front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated testing wasn't enough&lt;/strong&gt;: Our unit-level tests weren’t very useful as we moved logic between architectures which required rewriting big blocks of code, and our end-to-end test coverage wasn’t enough on its own, so a lot of manual testing was required.  We’re left with better tests and coverage across the board now.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What would we change for future migrations?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Establish solid end-to-end testing first&lt;/strong&gt;: We could test our most critical journeys, but there were more complex scenarios which weren't automated at the time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build more abstractions&lt;/strong&gt;: When we abstracted framework features like runtime configuration, navigation/routing, etc. it became simple to make them support Pages Router, App Router and both, and we chose to keep some of these abstractions in the long-term.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freeze other development&lt;/strong&gt;: Dual-running posed interesting challenges, but &lt;em&gt;especially&lt;/em&gt; now we know where the workflow changes will be, it’s simpler not to dual-run and to freeze development for a short while.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Was it worth it?
&lt;/h3&gt;

&lt;p&gt;Overall &lt;strong&gt;yes&lt;/strong&gt;. Between our rationalization work and the new architecture of React &amp;amp; Next.js’s App Router, this product now has a solid foundation for the next few years of feature development. And we now know where the rough-edges of a migration are for future App Router migrations!&lt;/p&gt;

&lt;h3&gt;
  
  
  Closing thoughts
&lt;/h3&gt;

&lt;p&gt;There are many people to thank, but in particular I want to thank my colleagues &lt;a href="https://linkedin.com/in/emanuele-vella" rel="noopener noreferrer"&gt;Emanuele Vella&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/quan-anh-le-b4b48b138/" rel="noopener noreferrer"&gt;Quan Le&lt;/a&gt; &amp;amp; &lt;a href="https://github.com/yoshi0701" rel="noopener noreferrer"&gt;Tomoaki Yoshioka&lt;/a&gt; for all their work during this major re-architecture and improvement project.  None of it would have been possible without them!&lt;/p&gt;

&lt;p&gt;We learned a &lt;em&gt;lot&lt;/em&gt;: And I hope that our lessons learned can both convince you to move to the App Router and re-embrace the server, as well as to save you some time along the way.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>nextjs</category>
      <category>react</category>
    </item>
    <item>
      <title>How I recovered my newsletter subscriptions</title>
      <dc:creator>Alex Wilson</dc:creator>
      <pubDate>Sun, 13 Nov 2022 22:07:56 +0000</pubDate>
      <link>https://dev.to/alexwilson/how-i-recovered-my-newsletter-subscriptions-1ed1</link>
      <guid>https://dev.to/alexwilson/how-i-recovered-my-newsletter-subscriptions-1ed1</guid>
      <description>&lt;p&gt;How many Substack accounts do you have?  Or rather, how many email addresses do you have?  This is the story of how collecting a few of both surprised me, how I recovered and how you can too.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: Substack is not at fault here, this was entirely my own doing!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Substack implements &lt;a href="https://www.ietf.org/rfc/rfc2919.txt"&gt;RFC-2919&lt;/a&gt; allowing us to programmatically get every list we've ever been subscribed to as &lt;code&gt;List-Id&lt;/code&gt; in any mail client.  Standards are &lt;em&gt;fantastic&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;With Gmail's search or API, we can retrieve emails from Substack using &lt;code&gt;from:substack.com&lt;/code&gt;.  And, to filter by payment receipts: &lt;code&gt;subject:"your payment receipt" from:substack.com&lt;/code&gt;.  Using Google Apps Script we can programmatically aggregate these messages into a handy list, &lt;a href="https://script.google.com/d/1SbHnmBnbn_MJohHDoV0DIGNpa4PDTIiidBBg0BQIY8Jn9MhnPK7GRC0X/edit"&gt;like this&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Newsletters being emails made it easy to retrieve this information, but, by backing this data up sooner I could have avoided the trouble altogether.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How I messed up
&lt;/h2&gt;

&lt;p&gt;Over the years I've used a few different email addresses.  With newsletters growing in popularity and so many being sent via Substack, that &lt;em&gt;also&lt;/em&gt; meant I'd collected several Substack accounts.  Thankfully &lt;a href="https://support.substack.com/hc/en-us/articles/360037489072-How-do-I-change-my-email-address-"&gt;it's easy to merge these accounts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While evaluating alternatives to &lt;a href="https://www.getrevue.co/"&gt;Reveue&lt;/a&gt;&lt;em&gt;(for no reason whatsoever 😇)&lt;/em&gt;, I misread Substack's onboarding flow and believing that I'd created another duplicate account, &lt;a href="https://support.substack.com/hc/en-us/articles/360060692511-How-do-I-delete-my-Substack-account-"&gt;hit delete&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Moments later, I realised that I'd deleted my primary account.  Whoops!&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;good&lt;/strong&gt; news: the deletion process is good.  My account was gone &lt;em&gt;immediately&lt;/em&gt;.  Many companies &lt;a href="https://justdeleteme.xyz/"&gt;make it difficult to leave&lt;/a&gt; so this was genuinely a welcome surprise.  Excellent compliance work folks!&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;bad&lt;/strong&gt; news: The deletion process is &lt;em&gt;too&lt;/em&gt; good.  I didn't receive a single message from the many newsletters I'd been unsubscribed from.  Oh no!&lt;/p&gt;

&lt;p&gt;Some newsletters are regular and predictable, but many are not.  These are the ones where you get the odd valuable unique insight or a punchy soundbite.  The few I might not know I missed, but, would miss the most.&lt;/p&gt;

&lt;p&gt;So: How do I find and recover all these newsletters that I've previously signed up for?&lt;/p&gt;

&lt;h2&gt;
  
  
  Email inboxes are just big log-files
&lt;/h2&gt;

&lt;p&gt;The nice thing about newsletters is that they're “just” emails.  And the nice thing about emails is that they're “just” files.  &lt;em&gt;Specifically&lt;/em&gt;, they are log files, or, a series of mails recorded in a standardised format.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is a slight oversimplification, because in reality emails can be stored in nearly any way, but the conceptual model still works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What this means is that it's possible to scan over all of these mails to reconstruct my history and to derive a list of the newsletters I read.  &lt;em&gt;Sweet&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Let's go about solving this problem like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find all mails sent from Subtack.com.&lt;/li&gt;
&lt;li&gt;Identify which newsletter they are from.&lt;/li&gt;
&lt;li&gt;Put those into a list.&lt;/li&gt;
&lt;li&gt;Optionally, group by the email address they were sent to.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Okay, from a busy inbox, how do we do that?&lt;/p&gt;

&lt;h2&gt;
  
  
  What do the standards say?
&lt;/h2&gt;

&lt;p&gt;This post isn't about the history of email - there's a fascinating series of IETF RFCs which build up the format we know today.&lt;/p&gt;

&lt;p&gt;The first standard which will help today is &lt;a href="https://www.ietf.org/rfc/rfc822.txt"&gt;RFC-822&lt;/a&gt;, the standard for the format of ARPA Internet Text Messages.&lt;/p&gt;

&lt;p&gt;Section 1.2 introduces headers and section 3, describes their format.  In short, email headers are bits of data about the email and are sent at the start of a message before the main content.  They look something 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;Subject: This is a subject
Content-Type: text/plain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which in this case says that the email's subject is "This is a subject" and that it's plain text.  This format is relatively easy for a computer to read.&lt;/p&gt;

&lt;p&gt;The next two standards that can help today are extensions specifically for mailing lists, specifically &lt;a href="https://www.ietf.org/rfc/rfc2369.txt"&gt;RFC-2369&lt;/a&gt; and &lt;a href="https://www.ietf.org/rfc/rfc2919.txt"&gt;RFC-2919&lt;/a&gt; which introduce a series of list-specific metadata.  These look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List-Owner: &amp;lt;mailto:example@example.org&amp;gt;
List-Unsubscribe: &amp;lt;https://example.org/unsubscribe&amp;gt;
List-Id: &amp;lt;some-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cool!  So maybe we can aggregate those List IDs?&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter: Gmail and Apps Script
&lt;/h2&gt;

&lt;p&gt;If I were dealing with a mail spool on a Linux machine, this might be a bit easier, because these fields are very &lt;code&gt;grep&lt;/code&gt;-able.&lt;/p&gt;

&lt;p&gt;e.g. &lt;code&gt;grep --only-matching 'List-Id:' | sort --unique&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;In practice, most of us are using webmail services and don't have direct access to the mail-box (although could download them over IMAP).  They &lt;em&gt;do&lt;/em&gt; offer APIs.  And Gmail, which I use, comes with a handy scripting engine called &lt;a href="https://www.google.com/script/start/"&gt;Apps Script&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Apps Script is great.  I've used it in Google Sheets to get Jira ticket descriptions and in Google Docs to create meeting agendas automatically.  It comes with a bunch of Google API integrations, including the &lt;a href="https://developers.google.com/apps-script/reference/gmail"&gt;Gmail Service&lt;/a&gt; which is one-click access to most Gmail operations.&lt;/p&gt;

&lt;p&gt;Our first task is iterating over all emails from Substack.  The filter &lt;code&gt;from:substack.com&lt;/code&gt; should do that for us, which looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GmailApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;from:substack.com&lt;/span&gt;&lt;span class="dl"&gt;"&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="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;threads&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;This is asynchronous code which is executing synchronously.  Apps Script is a bit weird like that.  For today's purposes it makes things much simpler.&lt;br&gt;
Okay, we now have a list of &lt;a href="https://developers.google.com/apps-script/reference/gmail/gmail-thread"&gt;Threads&lt;/a&gt;.   Threads don't include much information about the messages, so our next step is to retrieve the &lt;a href="https://developers.google.com/apps-script/reference/gmail/gmail-message"&gt;Messages&lt;/a&gt; and read their &lt;code&gt;List-ID&lt;/code&gt; headers.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Find last 50 emails from Substack.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GmailApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;from:substack.com&lt;/span&gt;&lt;span class="dl"&gt;"&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="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thread&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Log the list ID of the first message in each thread.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getMessages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;getHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;List-ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excellent, now we're getting somewhere.  We have list IDs for newsletters.  Our next step is to iterate over all emails, storing unique list IDs only.  We can use a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set"&gt;Set&lt;/a&gt; for these, which only hold unique values.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// Find last 50 emails from Substack.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;GmailApp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;from:substack.com&lt;/span&gt;&lt;span class="dl"&gt;"&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="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;thread&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Retrieve list ID of the first message in each thread.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getMessages&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;getHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;List-ID&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;// Store the list ID if it's unique.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listId&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nx"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listId&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="c1"&gt;// Log the unique list IDs.&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lists&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have a list of the unique lists we have at some point been subscribed to, excellent!  &lt;/p&gt;

&lt;p&gt;With a bit of refactoring, we can group this by email - I have done this and you can see the code further down so won't cover it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Show me the code!  And how do I run this?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Excellent question and the answer to both questions is here: &lt;a href="https://script.google.com/d/1SbHnmBnbn_MJohHDoV0DIGNpa4PDTIiidBBg0BQIY8Jn9MhnPK7GRC0X/edit"&gt;What are my Substack subscriptions?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this example, I've implemented pagination using a Generator (Apps Script makes &lt;a href="https://javascript.info/async-iterators-generators"&gt;AsyncIterables&lt;/a&gt; very easy!)&lt;/p&gt;

&lt;p&gt;This example also uses a non-standard field &lt;code&gt;List-URL&lt;/code&gt; which works for Substack and is a more human-friendly.  It also groups by &lt;code&gt;Delivered-To&lt;/code&gt; email address making it easier to find duplicates.&lt;/p&gt;

&lt;p&gt;Instead of logging the result, this sends you an email using the &lt;a href="https://developers.google.com/apps-script/reference/gmail/gmail-app#sendEmail(String,String,String,Object)"&gt;sendEmail&lt;/a&gt; method.&lt;/p&gt;

&lt;h2&gt;
  
  
  The manual bit
&lt;/h2&gt;

&lt;p&gt;Unfortunately, Substack doesn't offer an API, so after getting a full list of which newsletters I'd previously been subscribed to, I couldn't automatically re-subscribe.  So I did a bit of clicking of links, which seemed like a fair compromise considering that I got into this fix by doing a bit of clicking on links.&lt;/p&gt;

&lt;p&gt;Annoyingly some subscriptions were ones I'd paid for and Substack doesn't have a way to automatically resume or refund these.  &lt;a href="https://support.substack.com/hc/en-us/articles/360059896552-How-do-I-cancel-a-subscription-and-issue-a-refund-for-a-paid-subscriber-"&gt;They recommend speaking to support&lt;/a&gt;, so armed with a similarly-obtained list of the paid-for subscriptions, I've done just that.&lt;/p&gt;

&lt;p&gt;You can get those using this query: &lt;code&gt;subject:"Your payment receipt" from:substack.com&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Always bet on Email
&lt;/h3&gt;

&lt;p&gt;Email has fallen in popularity, even as newsletters have surged, and until today I actually wished that more of these newsletters were RSS feeds instead.&lt;/p&gt;

&lt;p&gt;But it's actually very handy being able to find conversations, receipts, notifications, news and so much more in one place.  And this kind of recovery would not be possible without that history.&lt;/p&gt;

&lt;p&gt;Email clients are ubiquitous and work on nearly every platform.  It's genuinely quite hard to beat the reliability of email.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Standards are dope. Why don't we use more of them?
&lt;/h3&gt;

&lt;p&gt;If you're building a platform doing something like sending out newsletters today, try and include their guidance.&lt;/p&gt;

&lt;p&gt;After recovering my Substack subscriptions I had a look at some other newsletter providers I'm subscribed through and the presence of mailing-list headers in their messages was spotty at best.&lt;/p&gt;

&lt;p&gt;It's not only email: For example on the web, we frequently see people re-inventing their own behaviour, especially around how content is made accessible or how page navigation works, even though browsers do both better.  I've definitely done this in the past.  We can do better.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Export your data and keep fewer accounts
&lt;/h3&gt;

&lt;p&gt;Most platforms offer some form of data export.  Take advantage of it - because it's always nice to have a backup!&lt;/p&gt;

&lt;p&gt;Also, by avoiding keeping many digital accounts you'll be far less likely to run into problems like these.  You will also reduce your risk of data theft or leaks.  &lt;/p&gt;

&lt;p&gt;.﻿..&lt;/p&gt;

&lt;p&gt;This journey has been a slightly unwelcome-yet-fun distraction, but I hope if not helpful that it's at least mildly interesting.  Feedback is welcome - thanks for reading!&lt;/p&gt;

</description>
      <category>email</category>
      <category>javascript</category>
      <category>gmail</category>
      <category>newsletters</category>
    </item>
  </channel>
</rss>
