Migrating to Verified Routes

Doorway from inside going to outside with a bright and shiny day.
Image by Annie Ruygt

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.

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:

Animated gif showing the mix task when executed.

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!

Fly.io ❤️ Elixir

Fly.io is a great way to run your Phoenix LiveView app close to your users. It’s really easy to get started. You can be running in minutes.

Deploy a Phoenix app today!