Migration to Next.js App Router
Musicn (Beta), now equipped with Next.js 14. But how hard was it to migrate?
I made an app called “Musicn”. It’s an app that allows users to discover music and share your music taste with others. Beta version of Musicn
It was a pretty old project, with any major change being about 2 years ago. The most significant thing after was moving to Chakra UI as the UI library.
The Next.js version of the project was 12, still using the page router. While it was sufficient for that time, I came back to work on the project to work on a nifty new feature that I have been planning for days...
So here I go, into the codebase...
The messy code
Coming back, I wasn’t really given a nice greeting with how messy the code is. Messy, as in, redundant functions, unneeded abstractions and the lack of code comments.
The lack of code comments made it hard for me to navigate and change the code.
Recently, I’ve watched advancements in Next.js. Introducing App Router and all the cool features but it was a bit of a jump for me, which is why I didn’t bother upgrading the Next.js of the project to version 13 at that point in time… Until recently…
Preparing myself for migration
I read the migration guide from Vercel themselves
https://nextjs.org/docs/app/building-your-application/upgrading/app-router-migration
And then I realised, why not make the switch and along the way, clean up the mess I’ve wrote?
I prepared myself for the hell to come, knowing that a lot of functionality will break. I prepared myself by creating a new branch (feat/migrate-nextjs
) and got my hands dirty.
Initial migration steps
According to the guide, we just have to install the latest Next.js, React, React DOM and the ESLint configuration. And then, we can incrementally adopt pages from the pages
directory to the app
directory.
Remember how I mentioned that I’ve watched advancements in Next.js and didn’t bother to upgrade? One of the main reasons is the seemingly different file structure. For example, why is there page.tsx
and why can’t we name the routes as is such as signup.tsx
anymore.
But after doing some reading and trying it out for myself, I actually thought the new way is genius! The page.tsx
serves as the main page, the content of the page while other tsx
files such as loading.tsx
serves as the component to show when the page is being loaded from the server (if it is an RSC) and error.tsx
serves as the component to show when there is an error.
The migration process was made simpler as the old pages from the pages
directory are still working, thank you Vercel for allowing me to incrementally adopt the app router.
A taste of React Server Components
At that point in time, the biggest feature that came with future version of Next.js was the use of React Server Components. Watching their Next.js 13 video, I am quickly amazed by how you can just call database functions from your React Components. It’s like magic! In the past, I’d have to expose an API route and make a HTTP request from the client. But now, I can just call database functions directly? You can imagine how mind-blown I was!
This, in result, cuts down the codebase by a lot. In the past, Musicn had a lot of API routes exposed, each of them has a lot of boilerplate code because I needed to make sure it was as typesafe as possible. But with this new addition, types are automatically inferred from our database calls (types inferred here refers to types from the Prisma client).
Moreover, React Suspense allows me to show a loading indicator as the page loads, this is a massive bump for user experience. In the past, because of getServerSideProps
, there wouldn’t be any loading indicator that the page I clicked is loading, all I got was a page that was frozen.
The trouble of migration
In the process of migration, there were a ton of hiccups. So much so that I devised a plan:
Migrate key app features first (exclude styling, and minor features)
Either prepend old API/page files with ‘_’ (i.e. ‘signup.tsx’ → ‘_signup.tsx’) OR place conflicting old API files in the ‘.old.api’ folder in the root of the project.
Implement proper access control in the now-migrated features.
Using this plan, I was able to migrate key features (except login/signup) within the first day of migration. And finished up the migration of other features the very next day.
But realistically, this is how it went:
Migrate features
Clean-up code
Test deploy
Build fails
Fix issue
Test deploy again
Repeating steps 3-6 for about 5 times.
If every failed build lowers my sanity by 10 points, I'd be dead by now.
Use client? Use server? What exactly am I supposed to use?
Yep, trouble arises! With the new 'use client' and 'use server' directive, it is now simpler to dictate whether a component should be a normal React component or a Server Component.
The impressive mind-blowing feature I mentioned earlier about calling database calls directly in your components? It is only available in Server Components, understandably. This is because the component is being processed by the server before sending it to the client.
Only pages that require SEO content is marked as Server Components. This includes: the users page, and the user profile page. Other than that, it is client components.
Does authentication have to be confusing? – Reviewing and tackling authentication (again)
In the past, Musicn’s authentication flow has been:
Log In/Sign Up with email and password
Link Spotify account to your Musicn account
Done
This simple authentication flow proved problematic as we cross the roads of Email activation, Password reset, Password changing and etc. Which is why I wanted to offload the authentication providers to services like Auth0.
But after meticulous planning, I found out that is it possible to just let users Sign In with their Spotify account via OAuth2.0 and let Musicn handle the creation of their Musicn account based on their Spotify profile.
So that’s exactly what I did!
Now, Musicn’s authentication flow is:
Log In using Spotify Account
If user doesn’t have a Musicn account, create one automatically based on their Spotify profile
Done
And also did I tell you that this process is further simplified using a library I found called Lucia?https://lucia-auth.com/
Lucia has helped immensely by abstracting the confusing parts of authentication and session management and I’ll forever be thankful for this library.
The main reason I used Lucia is because it is both customizable and un-opinionated. Allowing me to store what I want in each session (that, btw, gets stored in my database via the Prisma Adapter for Lucia). And customizable in a sense that I am allowed to pass in the scopes needed to retrieve what I need from the access token.
Conclusion
From migration to code clean-up, this experience has been humbling for me. This is what sets apart hobbyists and realists. As hobbyists, it would’ve been my instinct to immediately create a new repo and start from scratch using Next.js 13 but I avoided doing that because in a real work scenario (as backed up by my internship), we tend to upgrade versions and not rebuild and I just have to have a taste of what it feels like to migrate to a major version of a framework.
With the new features added in Next.js 13, I am just glad that the codebase got much more readable and cleaner. And I am really hoping to see what the future of Next.js brings with React Developer Team’s advancements in React – such as introducing form actions.