The Better Migration
@imarikchakma · Aug 1, 2025
When I joined roadmap.sh, it was a content-heavy application with little bit of interactivity. We were using Astro for the static content and React for the interactive parts.
Fast forward to today: it’s now a feature-rich application with lots of interactive parts and complex features like AI Tutor, Courses, Roadmap Editor, and more.
tl;dr: We migrated the account and teams pages to React Router for now — more to come.

For some context on the size of the application:
- 2.2M+ users
- 18k+ teams
- 7M visits per month
Astro was the best choice for the static, content-heavy parts of the application. But for the interactive parts, it started showing limitations. That’s when we began looking for a better alternative — and that’s where React Router came into the picture.

Why React Router?
The main issue was full page reloads in Astro as it was a multi-page application. Which is expected as it’s a static site generator. We could have used React Router inside the Astro app with wildcard routes, but we wanted to keep the Astro app as simple as possible.
After some research, we found that React Router is the best option for our use case. We were already using it in some of our internal tools and it was a great fit for our use case. We can pre-render some part of the app on the build time and then use React Router to handle the rest of the routing.
If you ask me, I would still use Astro for the static content. But for a feature rich application and complex features, React Router is the best option.
Why Incremental Migration?
We didn’t want to go for a full-blown migration right away. Instead, we decided on an incremental migration strategy:
- Start with the account and teams pages.
- Gradually move features like AI Tutor and others.
This approach allows us to ship improvements without breaking the whole platform.
Tech Stack Upgrades
We were already using React Query for async state management, fetching, and mutations (and I’m a huge fan of it, btw). But we weren’t using it to its full potential.
So, we decided to migrate all async states to TanStack Query. Paired with React Router, this combo now gives us:
- Strong type safety
- Smart caching
- Optimistic updates
- Seamless data loading with routing
// (imports omitted for brevity)
export async function clientLoader(args: Route.ClientLoaderArgs) {
redirectIfNotLoggedIn();
// async work runs here before rendering
// combined with the HydrateFallback component
}
export default function AccountSettingsPage(props: Route.ComponentProps) {
const [currentUser, billingDetails] = useSuspenseQueries({
queries: [currentUserOptions(), billingDetailsOptions()],
});
// rest is just UI code
}
Keep it simple, stupid.
Running Two Apps on the Same Domain
The interesting part is how we run two apps on the same domain. The magic happens in our nginx config.
We had a single app running, so the config was simple. Every request is proxied to the upstream server.
upstream roadmapsh_upstream {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name roadmap.sh;
location / {
proxy_pass http://roadmapsh_upstream;
}
}
Now, we’ve added another upstream for the new React Router app:
upstream roadmapsh_upstream {
server 127.0.0.1:3000;
}
upstream app_roadmapsh_upstream {
server 127.0.0.1:4000;
}
server {
location ~ ^/(account|teams)(/.*)?$ {
proxy_pass http://app_roadmapsh_upstream;
}
location / {
proxy_pass http://roadmapsh_upstream;
}
}

Astro app → still runs on port 3000
, the new React Router app → runs on port 4000
and any requests starting with /account
or /teams
get routed to the new app.
Wrapping Up
This was just an overview of the setup. In reality, we’re running a more complex system with multiple domains and subdomains, but the main idea stays the same. These images are drawn by @kamrify.
We are happy with the result and we are looking forward to migrate more features to React Router.
I am also keeping an eye on Tanstack Start though :D
If you have any questions, feel free to reach out to me on X.