Learning Elixir and Building a SaaS with Phoenix Framework

Learning new programming languages isn’t always necessary to grow as a developer. You can spend years focusing on just one language and still feel like there’s so much more to learn. Programming languages, much like spoken languages, are built from simple building blocks.

Think about how we use words to form sentences, sentences to create paragraphs, and paragraphs to convey meaning. Letters and words can be combined in endless ways. The same goes for programming language syntax.

Why Learn a New Programming Language?

If you’ve been coding in one language for over a decade, you probably have the skills to build almost anything you can imagine. So, why bother learning a new language?

For me, it was a mix of curiosity, a bit of FOMO, and that classic “grass is greener” feeling. But mostly, it came down to two things I’m passionate about: learning and creating.

Most of my career has revolved around JavaScript in one form or another. Back in the day, before Node.js, we’d sprinkle bits of JavaScript to add interactivity to mostly static web pages. Now, JavaScript has grown into a powerful beast capable of just about anything. It’s a little messy at times, but there’s a strange beauty in that chaos.

Along the way, I’ve also dabbled in ASP, .NET (C#), Java, Pascal, PHP, Go, Rust, Flutter, Haskell, and a few others.

Out of all these, only one is a functional language: Haskell.

Haskell never really clicked for me. I could admire the elegance in others’ code, but writing it myself always felt challenging. Maybe it’s because Haskell is so precise and mathematically strict, while my brain tends to lean more towards the creative and chaotic side.

At first glance, Elixir seemed like a friendlier functional language than Haskell. Even though I never fully connected with Haskell, I appreciated many of its concepts. Elixir felt like a good middle ground—combining the best of Haskell’s rigor with the flexibility of Python or Ruby.

Elixir has been getting a lot of attention lately. Big players like Heroku, Discord, and WhatsApp have all adopted it successfully at scale.

So, I made it my goal to learn Elixir in 2021.

Learning Elixir

The official Elixir website sums it up nicely:

Elixir is a dynamic, functional language for building scalable and maintainable applications. Elixir leverages the Erlang VM, known for running low-latency, distributed, and fault-tolerant systems. Elixir is successfully used in web development, embedded software, data ingestion, and multimedia processing, across a wide range of industries.

Everyone learns differently, and I usually learn best by diving in and doing. But this time, I decided to slow down and start by reading.

I focused on these resources:

These gave me the confidence to start building my own project.

Building a SaaS in Elixir

Since launching TAYL in 2019, I’ve been exploring what AI can do for audio. TAYL lets you turn any text—from websites to documents—into audio you can listen to on iOS, Android, as a podcast, or through the web app. Thousands of users worldwide have enjoyed the convenience of listening instead of reading.

To test my new Elixir skills, I decided to build on what I learned from TAYL. That way, I could always fall back on my existing Node.js solutions if I got stuck.

This new project was a SaaS aimed at helping website owners convert their written content into audio. Initially, it focused on audio, but I later added video support. The app would then distribute audio and video to platforms like Apple Podcasts, Spotify, and YouTube.

The project had many moving parts that needed to work smoothly together:

  • User accounts and authentication.
  • Subscription payments and one-time purchases.
  • Requesting and scraping web pages:
    • Handling broken or invalid HTML markup.
    • Extracting metadata like title, author, article body, featured image, publish date, categories, and more.
    • Producing a simplified version of the page with just the article content.
  • Converting HTML articles into SSML (Speech Synthesis Markup Language).
  • Requesting and parsing RSS feeds:
    • There are three main RSS specifications, but many websites don’t fully comply with them.
  • Periodically fetching new content from RSS feeds.
  • Audio processing:
    • Concatenating multiple files.
    • Cross-fading clips.
    • Encoding audio to balance quality and file size.
  • Video processing:
    • Generating video frames from an HTML/CSS template with article metadata.
    • Encoding video with embedded audio.
  • Downloading and uploading to cloud storage.
  • Generating podcast RSS feeds from episode data.
  • Publishing video files to YouTube.
  • Searching and browsing soundtracks from a partner library.

At first, the scope was much smaller. But as I realized how quickly I could iterate and build, I kept adding features.

Test-Driven Development in Elixir

Following German’s book, I embraced test-driven development (TDD) throughout the project. In Elixir, unit tests are typically written with ExUnit.

For the best experience, I recommend integrating your test environment with your editor or IDE. I found vim-test worked perfectly for me!

When you do TDD right, you can build an entire web app without ever opening a browser. Phoenix Framework, which I used for this project, has excellent support for testing.

Here’s an example of how we can assert things based on HTML-like selectors. If you’ve tested modern UI libraries like Vue or React, this will look familiar:

  describe "PodcastLive :index" do
    test "displays podcast title and description", %{conn: conn, user: user} do
      podcast = insert(:podcast, user: user)

      {:ok, view, _html} = live(conn, Routes.podcast_path(conn, :index, podcast.id))

      assert has_element?(view, "h2", podcast.title)
      assert has_element?(view, "div", podcast.description)
    end

    test "lists podcast episodes", %{conn: conn, user: user} do
      episodes = insert_list(3, :episode)
      podcast = insert(:podcast, user: user, episodes: episodes)

      {:ok, view, _html} = live(conn, Routes.podcast_path(conn, :index, podcast.id))

      for episode <- episodes do
        assert has_element?(view, "[data-test=episode_title]", episode.title)
      end
    end

    test "lists podcast feeds", %{conn: conn, user: user} do
      feeds = insert_list(3, :feed)
      podcast = insert(:podcast, user: user, feeds: feeds)

      {:ok, view, _html} = live(conn, Routes.podcast_path(conn, :index, podcast.id))

      for feed <- feeds do
        assert has_element?(view, "[data-test=feed_title]", feed.title)
      end
    end
  end

We start with the end goal in mind when doing TDD. In this example, we test that the page displays the podcast’s title and description, along with lists of episodes and feeds.

Results

I deployed the app to Google Cloud. It could have run anywhere, but since I chose Google Cloud Storage for hosting, it made sense to keep everything there. In the future, I plan to move parts of the app to fly.io to take advantage of their edge network and improve performance.

So far, only a handful of customers have signed up. We’ve done minimal marketing, so it’s too early to tell if it’s a hit or miss. Either way, I’m happy with the progress.

Podopi

Check out podopi.com to see it in action. The landing page was built with Next.js and TailwindCSS. If you have a website and want to offer your readers audio content, sign up for a free trial.

This video was made using a few screen recordings and code. Since I didn’t have any good video editing software, I figured, why not use code? The voiceover was created using some of the AI voices available in Podopi, and it worked surprisingly well!

Conclusion

Building this project took about three months, all while juggling other work and trading stocks to make a living. It took me a few weeks to get comfortable with Elixir and functional programming, but after that, I felt just as productive—if not more so—as I do with JavaScript and Node.js.

Taking a test-driven approach helped me iterate quickly and learn effectively. There were many moments when I wasn’t sure how to tackle a problem in Elixir, but writing tests with ExUnit allowed me to try multiple solutions and pick the best one.

Elixir, especially with Phoenix Framework, is the “batteries included” solution I’d been searching for. Paired with Oban, a task queue built on Postgres, I feel empowered to build scalable applications fast.

Elixir isn’t a silver bullet, of course, but it’s a valuable addition to my toolbox that I’m excited to use again in the future.

ElixirErlangPhoenix FrameworkPhoenix LiveViewProgrammingEntrepreneurshipSaaSB2BPodopi