Phoenix 1.7: A Major Step for the Phoenix Framework

Phoenix 1.7: A Major Step for the Phoenix Framework

Discover the exciting features of Phoenix 1.7 that will change and improve your development experience.
Micky Jittjana
Micky Jittjana
July 20, 2023

Table of Contents


Phoenix 1.7 is here! The new version introduces several new exciting features that improve the overall experience for both users and developers. This article discusses the most notable features, such as a unified HTML rendering approach across the Controller and LiveView, built-in support for TailwindCSS, Verified Routes, and LiveView Streams.

Unified HTML rendering approach

Let’s start with the major change that impacts the application the most: the unified HTML rendering approach. Previously, the way to render components or partials among controller-based templates (also called Dead Views by José Valim and Chris McCord) and LiveView differed.

Normally in controller templates, we need to use the render function then pass the view module and assign it to the component:

<%= render(MyAppWeb.UserView, "user_card", user: user) %>

Meanwhile, LiveView uses a function component syntax.

<.user_card user={@user} />

Now Phoenix 1.7 supports function components to render both controller-based and LiveView-based components. Phoenix.View is removed and replaced with the new Phoenix.Template to unify the rendering approach.

The deprecation of Phoenix.View and rendering all components with the function-based approach allows components and layouts to be shared among controllers and LiveView modules.

# The following syntax can be used in both 
# controller and LiveView templates
<.user_card user={@user} />

The new project structure

Phoenix introduces a new directory structure corresponding to changes in component rendering.

The new project structure
The new project structure

The first noticeable change is the new structure no longer includes the views and templates directories due to the deprecation of Phoenix.View. Instead, view modules follows the naming convention *_html.ex or *_json.ex , depending on the response format, and are located within the controllers directory alongside templates.

The new structure also introduces the components directory, which contains components and layouts that can be used across controller and LiveView templates. For a new project, Phoenix will generate core_components.ex module, which contains the base UI for the application, such as modal, or button.

Built-in support for Tailwind CSS

After creating a Phoenix 1.7 application, it will already be set up to utilize Tailwind CSS as the default styling tool. It means that you can quickly launch the Phoenix server and start customizing your content using Tailwind utility classes. And there is no need to set up any extra tools like the Node.js or Webpack, everything works out of the box.

Tailwind CSS is a utility-first CSS framework which makes it easier to create shared components. By using Tailwind, the application’s stylings are in the same place as markup.

Instead of:

.content {
  display: flex;
  align-items: center;
  background-color: white;
  padding: 0.25rem;
<div class="content"></div>

We can write:

<div class="flex items-center bg-white p-1"></div>

This approach eliminates the need to name a class, group the same concern in the same file, and improve the code portability. We can copy the entire component to a different project without worrying about different ways of structuring CSS or custom abstractions.

We can define our custom styles inside tailwind.config.js.

module.exports = {
  theme: {
    colors: {
      'white': '#ffffff'

We can also use arbitrary properties.

<div class="bg-[#ffffff]"></div>

Note that we can still combine Tailwind CSS with the traditional stylesheet by using @apply directive (even though the official documentation does not recommend doing it):

.content {
  @apply flex items-center bg-white p-1;

Example of creating a shared component

attr :navigate, :any, required: true

def back(assigns) do
    <.link navigate={@navigate} class="white">

The purpose of this component is to display a back button. It receives one attribute, which is a path.

<.back navigate={~p"/posts"} />

It can be reused throughout the project. When updates are necessary, only a single file needs to be edited. Additionally, because we use Tailwind CSS, all the stylings are editable within a function. Multiple classes can be combined without worrying about the CSS specificity.

Verified Routes

Verified Routes is the new way to reference a valid path in templates or controllers, replacing the need to call route helpers. Previously, we would need to use the route helper and provide arguments to return a single path.

We can explicitly define a path inside the ~p sigil with Verified Routes and rely on compiler checks.

# Generate a route with the helper
MyWeb.Router.Helpers.user_path(@conn, :index)
# => "/users"

# Verified route is here!
# => "/users"

The first obvious benefit of Verified Routes is the convenient way of calling without the need to provide conn and action parameters. Moreover, the verified routes provide a compile-time check to ensure the routes are valid and exist. Phoenix will warn you at compilation time if any given routes are invalid or missing.

For example, the index screen contains a link to the dashboard but there is a typo in the given route.

<.link href={~p"/dashboardd"}>Dashboard</.link>

When starting the application, Phoenix will warn there is an invalid route.

$ mix phx.server
warning: no route path for MyWeb.Router matches "/dashboardd"
  lib/my_web/components/layouts/_header.html.heex:2: MyWeb.Layouts._header/1

Up to this point, you may find Verified Routes are the clear winner compared to routes helper, but we only cover the basic usage. In the next section, we will go through more practical use cases that will suit all needs in your project.

Params in Verified Routes

Params can be given as part of the path the same way as string interpolation.

# => "/users/1"

# => "/articles/elixir-is-awesome"

You can omit the key if the given params derive Phoenix.Param. By default, the key is id.

# => "/users/1"

However, it is possible to specify a custom key for the struct or schema.

# Specify custom key in the schema
defmodule Article do
    @derive {Phoenix.Param, key: :slug}
    schema "articles" do
        # ...
# Now you can omit the slug key!
# => "/articles/elixir-is-awesome"

Query string in Verified Routes

You can manually add a query string to the route.

# => "/users?active=true&search=John"

Another approach is to provide the query string as a map or keyword list.

~p"/users?#{%{active: true, search: @keyword}}"
# => "/users?active=true&search=John"

~p"/users?#{[active: true, search: @keyword]}"
# => "/users?active=true&search=John"

Set Up Verified Routes

The new project generated with Phoenix 1.7 generator supports Verified Routes out-of-the-box!

If your project uses a previous version of Phoenix, you will need to make some modifications to your codebase to enable this feature.

Here are the step-by-step instructions:

Update your web application module, lib/my_app_web.ex:

# Add static_paths function
def static_paths, do: ~w(css fonts images js favicon.ico robots.txt)

def controller do
  quote do
    # ...
    # Remove the `Router.Helpers` alias
    # Add verified_routes function, it is defined below.

defp view_helpers do
  quote do
    # ...
    # Add verified_routes function, it is defined below.

# Define verified_routes function
def verified_routes do
  quote do
    use Phoenix.VerifiedRoutes,
      endpoint: MyAppWeb.Endpoint,
      router: MyAppWeb.Router,
      statics: MyAppWeb.static_paths()

Update the static path of Plug.Static in lib/my_app_web/endpoint.ex:

plug Plug.Static,
    at: "/",
    from: :myapp,
    gzip: false,
    only: MyAppWeb.static_paths() # Update the static path here.

Add verified_routes to conn_case. This will allow your tests to use Verified Routes:

using do
    quote do
        use MyAppWeb, :verified_routes
        # ...

LiveView Streams

With LiveView Streams, you can now handle updates, inserts, and deletes for large datasets without storing and reloading the entire collection on the server like you would with a normal LiveView assignment.

To create a stream, call the stream/3 function, passing in the socket, the stream name, and the initial dataset:

def mount(_params, _session, socket) do
  {:ok, stream(socket, :books, Library.all_books())}

Then, in the template, instead of accessing a @books assign, you can use @stream.books.

If the collection’s contents change, you can use stream_insert/4 or stream_delete/3 to populate changes to the stream.

This not only helps to improve performance but also reduces the amount of re-rendering required to make changes on the front-end layer.

Migration from version 1.6

We upgraded a few of our Phoenix 1.6 (and even 1.5) apps to a newer version, and the process went smoothly. We did not come across any issues along the way.

Chris McCord has created this handy document on how to upgrade from Phoenix 1.6 to 1.7:


The Phoenix 1.7 update brings a lot of impressive new features and enhancements, particularly for developers working with LiveView. This release solidifies Phoenix’s standing as one of today’s most admired Web frameworks.

If you are unfamiliar with the Phoenix Framework, now is the perfect time to begin learning it with this official getting starting guide. You also can check our ready to go elixir template and our elixir code conventions.

If this is the kind of challenges you wanna tackle, Nimble is hiring awesome web and mobile developers to join our team in Bangkok, Thailand, Ho Chi Minh City, Vietnam, and Da Nang, Vietnam✌️

Join Us

Recommended Stories:

Accelerate your digital transformation.

Subscribe to our newsletter and get latest news and trends from Nimble