The Biggest Lessons I Learned While Deploying My First Phoenix App

In this blog post, I discussed building a blog site using the Phoenix framework and highlighted key lessons from deploying with Fly.io and implementing test-driven development.

For those who have not had the pleasure of having heard of the Phoenix framework, it is a web framework written in Elixir. Currently, at version 1.7.12, the Phoenix framework is quite mature and can certainly be the backbone of a fully fledged enterprise-grade SaaS project or simply be the trusty companion of a small side project (like what you’re currently reading this blog post from!).

In this blog post, I will share what I believe are some valuable lessons for a Phoenix first-timer. At the end of the article, you should have a clearer idea on whether using Phoenix might be a good idea for you.

Be careful with the code that you put into the app.js file

One of the great parts of Phoenix is a feature called LiveView, for LiveView to work, a JavaScript-enabled client runs a bit of code that is held in the app.js file.

// app.js
import "phoenix_html"
// Establish Phoenix Socket and LiveView configuration.
import {Socket} from "phoenix"
import {LiveSocket} from "phoenix_live_view"
import topbar from "../vendor/topbar"

let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
let liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: {_csrf_token: csrfToken}
})

topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

// connect if there are any LiveViews on the page
liveSocket.connect()
window.liveSocket = liveSocket

The magic lies in the call to liveSocket.connect(), this line is responsible for setting up a WebSocket connection from the client to the LiveView process on our server.

So how did this go wrong? I decided that it would be a good idea to add some nice animations to my splash page. My app.js looked like this:

import "phoenix_html"
import { Socket } from "phoenix"
import { LiveSocket } from "phoenix_live_view"
import topbar from "../vendor/topbar"

// Animations
import { animateFadeDown, animateTranslateUpDown } from "./animations/homepageAnimations";

const csrfToken = document?.querySelector("meta[name='csrf-token']")?.getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
  longPollFallbackMs: 2500,
  params: { _csrf_token: csrfToken }
})

// Homepage Animations added here:
animateFadeDown("#home-hero-content-section");
animateTranslateUpDown("#home-hero-sakura", 5);

topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" })
window.addEventListener("phx:page-loading-start", _info => topbar.show(300))
window.addEventListener("phx:page-loading-stop", _info => topbar.hide())

liveSocket.connect()
window.liveSocket = liveSocket

Two new function calls were added:

  • animateFadeDown("#home-hero-content-section");
  • animateTranslateUpDown("#home-hero-sakura", 5);

You’ll notice that the element selectors look quite specific, and it’s important to note that these selectors are unique to elements that only appear on the home page. However, the app.js script runs every time a page is opened - even those that are not the homepage. This meant that every time we would load the CV page, for instance, the animate functions would throw an error and there was nowhere in the code where this error was being caught. Since the error was not caught, the app.js script would stop executing at the first animation function and the rest of the script would not run, including the call to liveSocket.connect().

You might think this issue was obviously a mistake—and you’d be correct. However, this bug stumped me for a surprisingly long time. I simply couldn’t believe that such a basic error was the culprit behind my app’s glitches, especially since the animations themselves worked perfectly. What added to my frustration was the fact that I had noticed the error messages in the browser console but overlooked them. Like many JavaScript developers, I’ve become somewhat desensitized to console errors. Often, they seem inconsequential due to the frequent minor glitches in our extensive lists of dependencies.

A short-term fix to the problem was to do this:

// app.js
if (window.location.pathname === '/') {  // Check if the current page is the homepage.
  try {
    animateFadeDown("#home-hero-content-section");
    animateTranslateUpDown("#home-hero-sakura", 5);
  }
  catch (error) {
    // This shouldn't error because of the above guard clause,
    // but it's nice to have just in case.
    console.warn(error);
  }
}

This works fine for now, but perhaps an even cleaner solution would have been to use the Phoenix Hooks feature.

Test-Driven Development (TDD) and Phoenix is awesome!

When I set out to build my personal website using the Phoenix framework, the standout feature wasn’t its complexity—it’s quite a straightforward project. What really sets it apart is the approach I took in building it: I strictly adhered to test-driven development (TDD). This meant that (nearly) every single line of code was written only after setting out the tests for it.

One of the big wins of choosing Phoenix for this project is the speed of running tests. It is blazingly fast, and I don’t say that lightly. This project’s full test suite executes in under two seconds. This is a massive advantage, particularly in comparison to tools like Cypress, which are geared more towards comprehensive end-to-end testing and naturally take longer to run. This swift testing cycle has made the development process not only efficient but also a lot more enjoyable.

Fortunately, because I didn’t have to test any complex JavaScript on the client, I could avoid using tools like Wallaby and Cypress, which would have slowed down the overall test time.

A great productivity hack for TDD and Elixir is to use a test watcher, similar to Bacon in Rust. A test watcher automatically runs tests each time you save a file, ensuring immediate feedback on your changes. Simply add {:mix_test_watch, "~> 1.0", only: [:dev, :test], runtime: false} to your mix.exs file’s dependencies to access a new command: mix test.watch.

Deploying to Fly.io is easy… too easy?

Deploying to Fly.io was an incredibly straightforward experience, especially appealing to newcomers in app deployment. However using a third-party cloud service, can sometimes create a false sense of security, leading developers to underestimate the complexities involved in deploying and maintaining a web application in a production environment.

fly launch

Yep, that was about it to get deployed. I didn’t have a clean Dockerfile, so I had to add some lines manually, but if I didn’t have one, I would have been deployed for the first time in under 3 terminal commands.

It’s important to be mindful of the concept of “simple is hard”. The developer experience offered by services like Fly.io involves abstracting away nearly all of the underlying complexities of cloud deployments and it does pay dividends to try to understand what’s going on under the hood.

Don’t get me wrong, I am super happy that I deployed with Fly, I am using the smallest possible box and the costs are minimal, however it would have been even cheaper to host with an equally small EC2 machine on Amazon, but this was just too easy to turn down.

Setting up continuous delivery with Fly.io was straightforward too. I found some great guides that helped me integrate GitHub Actions for continuous deployment. This setup lets me automatically update my site whenever the tests pass. Here’s a particularly helpful guide: Continuous Deployment with GitHub Actions - https://fly.io/docs/app-guides/continuous-deployment-with-github-actions/.

Conclusion

I had a great time building this blog website with the Phoenix framework. It was really refreshing to work with something different from React and TypeScript. Elixir and Phoenix made the process enjoyable and straightforward.

I’m already planning my next projects, and I’ve got a couple of exciting ideas that I’ll definitely be using Elixir and Phoenix for. This experience has really shown me how powerful and flexible Phoenix can be, and I’m looking forward to diving back in.

Thanks for following along with my journey into Phoenix. I hope my experience helps you if you’re thinking about trying it out. Keep an eye out for more updates as I start on these new projects!