We’re Fly.io. We run apps for our users on hardware we host around the world. This post is about how to migrate from legacy Phoenix routes to the new Verified Routes in Phoenix 1.7. Fly.io happens to be a great place to run Phoenix applications. Check out how to get started!
Verified Routes were introduced in Phoenix 1.7. The upgrade guide strongly suggests upgrading to Phoenix 1.7 first and keeping your routes the same to begin with. Then later, you can migrate the routes as needed.
Well, it’s later. Now let’s figure out how to migrate those routes!
Problem
While the application is working fine as-is, you want the benefits of Verified Routes. Namely, those are:
- Easier to read
- Easier to write
- Shorter and more elegant: No route helper function with a bunch of arguments
- Compile-time checked: Even though they look like a normal string
You have an Elixir Phoenix project that was upgraded to Phoenix 1.7+. A lot of code was written before Verified Routes were available and now you’d like to migrate over to use them. The project is large and there are a lot of routes to convert and files to update. How can we best migrate to use Verified Routes? What can we expect from the process?
Solution
I went through the migration process with a project and took notes along the way. I’ll share what worked, what didn’t, what I did, and what I learned.
There is a slick tool to help with the conversion process. It’s a mix task created by Andreas Eriksson and can be found in this Gist. This tool was really helpful and does the bulk of the grunt work.
However, before we create and run the mix task, we’ll do a check and perform a manual conversion if needed. Let’s take a peek at what’s ahead.
- A quick, manual pre-migration stop - But only if needed.
- Tool:
mix convert_to_verified_routes
to migrate the bulk of it. - What doesn’t get migrated - and how we deal with it.
- Updating components that use verified routes
- Finishing up
TIP: Before starting, make sure you have a clean git workspace so you can easily review and revert changes if needed.
Before we dive in and start the migration, we’ll do a quick check and manual migrate any static routes.
A Manual Static Path Conversion
The mix task we’ll use breaks if it encounters any static routes. Thankfully, they are easy to find and convert.
Do a search for “Routes.static_” to see if any exist in the project. If not, move right along
Found a static route?
Static paths break things
If you have any static path routes that use Routes.static_path/2
like the following, they need to be converted manually.
Routes.static_path(@socket, "/images/logo-100px.png")
These completely confuse the script. They break the conversion process and it won’t detect more routes once this is encountered. They need to be handled manually. Thankfully, there are probably very few of them and the conversion is really straightforward.
Convert from:
Routes.static_path(@socket, "/images/logo-100px.png")
To:
~p"/images/logo-100px.png"
At least it’s easy!
A Tool to Help Migrate
The tool is a mix task found in this Gist. The mix task has a dedicated blog post: Automatic conversion to verified routes and it looks something like this when run:
With any static routes dealt with, we’re ready to start the migration! Let’s see how far we can get using the mix task.
Create the Mix Task
Create the file my_app/lib/mix/tasks/convert_to_verified_routes.ex
and copy the contents of the Gist into it. For reference, here’s what the Elixir School has to say about Custom Mix Tasks.
Next, update the @web_module
to match the one in the application being migrated.
@web_module MyAppWeb
Before we can run the mix task, we need to compile our project so the mix task becomes available: mix compile
We’re ready to run the script!
Running the Mix Task
With the mix task ready, let’s start converting! 🤞
We execute the mix task like this:
mix convert_to_verified_routes
What does it do?
When we run the mix task, it does a regex search through the source code of the project and finds instances like Routes.my_route_path(socket, :index)
and prompts to convert them to the appropriate verified route.
The prompt looks something like this:
The capital "Y"
at the end of the prompt means it’s the default action and we can just hit ENTER to accept the change.
Should we replace
Routes.shop_specials_index_path(socket, :index)
with
~p"/shop/specials"
[Yn]
The task searches through the full project, including test files. 🎉 It does a pretty decent job of converting most of your routes. It even detects and substitutes most query parameters!
What Doesn’t Get Migrated?
The mix task does not do 100% of the conversion. For instance:
- It doesn’t convert multi-line routes.
- It doesn’t handle some query parameters correctly.
- Routes in comments are not converted. This is more of a “heads-up” than an issue.
- If any routes are invalid, they are not updated and replaced. This makes sense, but there are no warnings of this either. You may first suspect that it didn’t work and then find out the route was bad all along!
To find routes that weren’t converted, search the project for Routes.
. Also, depending on the project, the conversion may result in some non-compiling code. On the plus side, that helps find where is didn’t work. This was the case for some of my query parameter conversions.
First, let’s come back to the issue with multi-line routes.
Multi-Line Routes
Long routes get wrapped by the Elixir formatter. These don’t get updated. For example:
<.link
patch={
Routes.admin_blog_section_show_path(
@socket,
:confirm_delete,
@blog,
@article
)
}
class="..."
>
The route was wrapped by the code formatter and crosses multiple lines. This route will not be converted automatically. To fix this, we can manually reformat the text up onto a single line and re-run the mix task. Lo and behold, it now gets converted!
Query Param Issues
There are a number of different ways to pass params to a route. Depending on what’s in the project, it may or may not have an issue.
In my project, I had some query params that were passed as a string. The URL query might look like this:
http://localhost:4000/partner/integration?by=enabled
When attempting to replace the params, this is what the conversion prompt looked like:
Should we replace
Routes.partner_integration_index_path(socket, :index, by: "enabled")
with
~p"/partner/integration?#{[by: \enabled\]}"
[Yn]
It didn’t handle the "enabled"
string literal correctly and it output invalid code. My usage was for sort order, so it was easy to search for [by: \
and find them all quickly. It helped seeing what it converted them to. Alternatively, we could say “no” to replace and handle it manually.
At the time, I looked into updating the script to handle this use-case, but I only had like 6 places where it was used. Since this was a one-time, one-way conversion, I was fine just doing the manual clean-up.
Components With Routes in Them
Separate from the migration process, there was another change I needed to make when moving to verified routes.
I had some components, like those for a sidebar menu, that had routes inside the
component. Here’s an example. Notice that “target” takes a verified route and it’s inside
the profile_sidebar
function component.
attr :active, :atom, required: true
def profile_sidebar(assigns) do
~H"""
<nav class="space-y-1">
<.sidebar_nav_item
title="Account"
name={:account}
icon="far fa-user"
active={@active}
target={~p"/account"} />
...
</nav>
"""
end
It showed up as the error: warning: undefined function sigil_p/2
.
The fix was to add the following line to the top of my component file.
use Phoenix.VerifiedRoutes,
endpoint: MyAppWeb.Endpoint,
router: MyAppWeb.Router
With that addition, I could use Verified Routes in a component. Yay!
Finishing Up
The mix task did the bulk of the work for me and converted 80 files. There were multiple passes of running the script, finding something that wasn’t converted, updating the project and running the script again.
After the migration was complete, there were no more Routes.
found in the project. My components were updated when using verified routes internally, the project compiled, and the tests pass. Done! 😅
The final cleanup was to delete the mix task from the project. No need to keep it around.
Discussion
There is no silver bullet to convert everything perfectly in one shot. Still, the mix task gets us really far! When we know what kinds of left-overs might exist and how to find them, we can pretty quickly get a project migrated over to use Verified Routes.
Once migrated, we get all of the lovely benefits of Verified Routes. Here’s a refresher on why Verified Routes are worth the effort.
- Much easier to write
- Much easier to read
- Shorter and more elegant
- Great compiler checks
Thanks to Andreas Eriksson for putting in the effort to make a tool like this available to the community!