# Building an URL shortener in a single file Elixir script

A couple of years ago, I discovered [elixir](https://elixir-lang.org), and since then, it has become my go-to language for creating full web applications, APIs, or just scripts.

Elixir comes with strong scripting capabilities, enabling us to write code in a file—like `some_script.exs`—and run it directly using `elixir some_script.exs`.

Most Elixir projects meant for production are compiled through mix, taking advantage of all possible optimizations and performance improvements. However, let's see what we can achieve when we focus on scripting and skip the compilation step!

Let's have some fun and write a simple URL shortener in one file using: [Phoenix](https://phoenixframework.org), [LiveView](https://hexdocs.pm/phoenix_live_view/Phoenix.LiveView.html), and [Erlang Term Storage (ETS)](https://elixirschool.com/en/lessons/storage/ets).

### Mix.install/2

`Mix.install/2` allows you to install any [hex](https://hex.pm/) package.

```elixir
Mix.install([
  {:bandit, "~> 1.1"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7.11"},
  {:phoenix_live_view, "~> 0.20.14"}
])
```

We install the latest version of bandit (pure Elixir HTTP server for Plug & WebSock applications), jason, phoenix and LiveView.

### Application.put\_env/4

We'll use [Application.put\_env/4](https://hexdocs.pm/elixir/Application.html#put_env/4) to configure the Application at runtime. Below is the base configuration we're going to use in this script:

```elixir
Application.put_env(:short_url, ShortUrl.Endpoint,
  adapter: Bandit.PhoenixAdapter,
  http: [ip: {0, 0, 0, 0}, port: 4001],
  url: [host: "localhost"],
  check_origin: ["//localhost"],
  server: true,
  live_view: [signing_salt: Enum.take_random(?a..?z, 10) |> to_string],
  secret_key_base: Enum.take_random(?a..?z, 64) |> to_string
)
```

I prefer this method over adding a `config` to `Mix.install/2` like this:

```elixir
Mix.install([ 
      {:bandit, "~> 1.1"},
      {:jason, "~> 1.0"},
      {:phoenix, "~> 1.7.11"},
      {:phoenix_live_view, "~> 0.20.14"}
    ],
    config: [
        short_url: [
            ShortUrl.Endpoint: [
                http: [ip: {0, 0, 0, 0}, port: 4001],
                url: [host: "localhost"],
                check_origin: ["//localhost"],
                server: true,
                live_view: [signing_salt: Enum.take_random(?a..?z, 10) |> to_string],
                secret_key_base: Enum.take_random(?a..?z, 64) |> to_string
            ]
        ]
    ]
)
```

I find the first method more readable than this one.

### We're set...

With just two functions and 16 lines of code, all contained in a single, portable file, we've established the foundation for our URL shortener.

### Link hash, storage and lookup

We're now ready to create short links, store them, and search for links by their hash key:

```elixir
defmodule ShortUrl do
  def permalink(bytes_count) do
    bytes_count
    |> :crypto.strong_rand_bytes()
    |> Base.url_encode64(padding: false)
  end

  def add_url(link, hash) do
    :ets.insert_new(:short_urls, {hash, link})
  end

  def get_url(hash) do
    :ets.lookup(:short_urls, hash)
  end
end
```

This simple module is straightforward: we generate a hash with the `permalink/1` function, then use `:ets.insert_new/2` to store the hash and the original link, and `:ets.lookup/2` to retrieve the link associated with the hash value.

### Phoenix LiveView

We're going to use Phoenix LiveView to display the short link generation form, generate the short url and show the user the new shortened url:

```elixir
defmodule ShortUrl.IndexLive do
  use Phoenix.LiveView, layout: {__MODULE__, :live}

  def mount(_params, _session, socket) do
    socket = assign(socket, short_url: nil)

    {:ok, socket}
  end

  def render("live.html", assigns) do
    ~H"""
    <script src="https://cdn.jsdelivr.net/npm/phoenix@1.7.11/priv/static/phoenix.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/phoenix_live_view@0.20.14/priv/static/phoenix_live_view.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
      liveSocket.connect()
    </script>
    <div class="bg-white py-16 sm:py-24">
      <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
        <div class="relative isolate overflow-hidden bg-gray-900 px-6 py-24 shadow-2xl sm:rounded-3xl sm:px-24 xl:py-32">
          <h2 class="mx-auto max-w-2xl text-center text-3xl font-bold tracking-tight text-white sm:text-4xl">One file url shortner</h2>
          <p class="mx-auto mt-2 max-w-xl text-center text-lg leading-8 text-gray-300">Try it out! Add your url in the form below!</p>
          <form phx-submit="generate" class="mx-auto mt-10 flex max-w-md gap-x-4">
            <label for="link-address" class="sr-only">Link address</label>
            <input id="link-address" name="link" type="url" required class="min-w-0 flex-auto rounded-md border-0 bg-white/5 px-3.5 py-2 text-white shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-white sm:text-sm sm:leading-6" placeholder="Enter your url">
            <button type="submit" class="flex-none rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">Make it short</button>
          </form>
          <div :if={@short_url}>
            <p class="mx-auto mt-2 max-w-xl text-center text-lg leading-8 text-gray-300">your short url is: <a href={@short_url} target="_blank"><%= @short_url %></a></p>
          </div>
        </div>
      </div>
    </div>
    """
  end

  def render(assigns) do
    ~H"""
    <div></div>
    """
  end

  def handle_event("generate", %{"link" => link}, socket) do
    short_url_hash = ShortUrl.permalink(8)
    short_url = ShortUrl.Endpoint.url() <> "/r/" <> short_url_hash

    ShortUrl.add_url(link, short_url_hash)

    socket = assign(socket, short_url: short_url)

    {:noreply, socket}
  end
end
```

### 301 redirects

Whenever a user visits the redirect endpoint, we will search for the hash and perform a 301 redirect to the matching URL:

```elixir
defmodule ShortUrl.RedirectController do
  use Phoenix.Controller

  def index(conn, %{"hash" => hash}) do
    case ShortUrl.get_url(hash) do
      [{_, link}] ->
        conn
        |> put_status(:moved_permanently)
        |> redirect(external: link)

      [] ->
        conn
        |> send_resp(200, "Link not found")
    end
  end
end
```

### One file URL shortener

```elixir
Mix.install([
  {:bandit, "~> 1.1"},
  {:jason, "~> 1.0"},
  {:phoenix, "~> 1.7.11"},
  {:phoenix_live_view, "~> 0.20.14"}
])

Application.put_env(:short_url, ShortUrl.Endpoint,
  adapter: Bandit.PhoenixAdapter,
  http: [ip: {0, 0, 0, 0}, port: 4001],
  url: [host: "localhost"],
  check_origin: ["//localhost"],
  server: true,
  live_view: [signing_salt: Enum.take_random(?a..?z, 10) |> to_string],
  secret_key_base: Enum.take_random(?a..?z, 64) |> to_string
)

defmodule ShortUrl do
  def permalink(bytes_count) do
    bytes_count
    |> :crypto.strong_rand_bytes()
    |> Base.url_encode64(padding: false)
  end

  def add_url(link, hash) do
    :ets.insert_new(:short_urls, {hash, link})
  end

  def get_url(hash) do
    :ets.lookup(:short_urls, hash)
  end
end

defmodule ShortUrl.IndexLive do
  use Phoenix.LiveView, layout: {__MODULE__, :live}

  def mount(_params, _session, socket) do
    socket = assign(socket, short_url: nil)

    {:ok, socket}
  end

  def render("live.html", assigns) do
    ~H"""
    <script src="https://cdn.jsdelivr.net/npm/phoenix@1.7.11/priv/static/phoenix.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/phoenix_live_view@0.20.14/priv/static/phoenix_live_view.min.js"></script>
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
      let liveSocket = new window.LiveView.LiveSocket("/live", window.Phoenix.Socket)
      liveSocket.connect()
    </script>
    <div class="bg-white py-16 sm:py-24">
      <div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
        <div class="relative isolate overflow-hidden bg-gray-900 px-6 py-24 shadow-2xl sm:rounded-3xl sm:px-24 xl:py-32">
          <h2 class="mx-auto max-w-2xl text-center text-3xl font-bold tracking-tight text-white sm:text-4xl">One file url shortner</h2>
          <p class="mx-auto mt-2 max-w-xl text-center text-lg leading-8 text-gray-300">Try it out! Add your url in the form below!</p>
          <form phx-submit="generate" class="mx-auto mt-10 flex max-w-md gap-x-4">
            <label for="link-address" class="sr-only">Link address</label>
            <input id="link-address" name="link" type="url" required class="min-w-0 flex-auto rounded-md border-0 bg-white/5 px-3.5 py-2 text-white shadow-sm ring-1 ring-inset ring-white/10 focus:ring-2 focus:ring-inset focus:ring-white sm:text-sm sm:leading-6" placeholder="Enter your url">
            <button type="submit" class="flex-none rounded-md bg-white px-3.5 py-2.5 text-sm font-semibold text-gray-900 shadow-sm hover:bg-gray-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white">Make it short</button>
          </form>
          <div :if={@short_url}>
            <p class="mx-auto mt-2 max-w-xl text-center text-lg leading-8 text-gray-300">your short url is: <a href={@short_url} target="_blank"><%= @short_url %></a></p>
          </div>
        </div>
      </div>
    </div>
    """
  end

  def render(assigns) do
    ~H"""
    <div></div>
    """
  end

  def handle_event("generate", %{"link" => link}, socket) do
    short_url_hash = ShortUrl.permalink(8)
    short_url = ShortUrl.Endpoint.url() <> "/r/" <> short_url_hash

    ShortUrl.add_url(link, short_url_hash)

    socket = assign(socket, short_url: short_url)

    {:noreply, socket}
  end
end

defmodule ShortUrl.RedirectController do
  use Phoenix.Controller

  def index(conn, %{"hash" => hash}) do
    case ShortUrl.get_url(hash) do
      [{_, link}] ->
        conn
        |> put_status(:moved_permanently)
        |> redirect(external: link)

      [] ->
        conn
        |> send_resp(200, "Link not found")
    end
  end
end

defmodule ShortUrl.ErrorView do
  def render(template, _), do: Phoenix.Controller.status_message_from_template(template)
end

defmodule Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  pipeline :browser do
    plug(:accepts, ["html"])
  end

  scope "/", ShortUrl do
    pipe_through(:browser)

    get("/r/:hash", RedirectController, :index)

    live("/", IndexLive, :index)
  end
end

defmodule ShortUrl.Endpoint do
  use Phoenix.Endpoint, otp_app: :short_url
  socket("/live", Phoenix.LiveView.Socket)
  plug(Router)
end

{:ok, _} = Supervisor.start_link([ShortUrl.Endpoint], strategy: :one_for_one)
:ets.new(:short_urls, [:set, :public, :named_table])
Process.sleep(:infinity)
```

In just one file, we've created a fully functional Phoenix LiveView application!

I aimed to keep the features minimal, but feel free to add more, like permanent storage (for example, PostgreSQL), initialize ETS when the application starts, and so on.

To run the application, use: `elixir app.exs` then visit: `http://localhost:4001`

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1710638491341/658a69eb-d02b-4006-8567-ee7bb304d951.png align="center")

### Bonus: let's create a Dockerfile for it

```dockerfile
FROM "hexpm/elixir:1.15.7-erlang-26.1.2-debian-bullseye-20240130-slim"

# install dependencies
RUN apt-get update -y && apt-get install -y build-essential git libstdc++6 openssl libncurses5 locales \
    && apt-get clean && rm -f /var/lib/apt/lists/*_*

RUN sed -i '/en_US.UTF-8/s/^# //g' /etc/locale.gen && locale-gen

ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8

WORKDIR "/app"

# Copy our file over
COPY app.exs /app

RUN mix local.hex --force && \
    mix local.rebar --force

EXPOSE 4001

CMD elixir /app/app.exs
```

Build with `docker build -t short_url .` and run it with `docker run -p 127.0.0.1:4001:4001 short_url` .

Now it's ready for production :).

Of course, this isn't code or an application ready for production, but I wrote this article to demonstrate the capabilities of Elixir, `Mix.install/2,` and what you can achieve with just one file and 100 lines of code.

If you're interested in seeing what else you can do with single-file Elixir scripts, I recommend checking out this GitHub repository: [mix\_install\_examples](https://github.com/wojtekmach/mix_install_examples/)
