How to upload a file in Elixir with Waffle

Article autor
August 11, 2025
How to upload a file in Elixir with Waffle
Elixir Newsletter
Join Elixir newsletter

Subscribe to receive Elixir news to your inbox every two weeks.

Oops! Something went wrong while submitting the form.

Table of contents

The ability to upload files is a key requirement for many todays web and mobile applications. In this tutorial, we will look at how we can accomplish file uploads to local storage and S3 server in Phoenix with the help of Waffle library.

Background

Waffle is a flexible file upload library for Elixir with straightforward integrations for Amazon S3 and ImageMagick. This library is forked from Arc and works much in the same way. To illustrate how to upload files we will start with a simple demo application, which let us upload pictures to local storage and viewing pictures on page. We will also use Waffle.Ecto library to integrate Waffle with Ecto and save file data in the database. At the end, we will check how to upload files to S3 server.

Demo app

It's time to create our app:

mix phx.new file_uploader

Let's take a few shortcuts and create everything that we need with one command in the project folder:

mix phx.gen.html Gallery Photo photos picture:string

The above command mix phx.gen.html generates controller, views, and context for an HTML resource. We will have a table named "photos" in the database with id, picture (for storing data about the file) and timestamps columns.

We also need to provide an appropriate route for our new controller in router.ex so update the existing scope just like below:

scope "/", FileUploaderWeb do
  pipe_through :browser
  get "/", PageController, :index

  resources "/photos", PhotoController
end

Now provide your database credentials in your config and run

mix ecto.create && mix ecto.migrate

to set up a database. You can run mix phx.server to start an application and go to http://localhost:4000/photos just to take a look at what we have created at the moment. In the next chapter, we are going to add Waffle and Waffle.Ecto to our project.

Setup Waffle

Add the latest stable release of Waffle and Waffle.Ecto to your mix.exs file:

defp deps do
  [
    ...
    {:waffle,  "~> 1.1.5"},
    {:waffle_ecto, "~> 0.0.11"}
    ...
  ]
end

and after that:

mix deps.get

Next, we should generate a definition module with mix waffle.g :

 mix waffle.g file_image

With above command we generated file in lib/[APP_NAME]_web/uploaders/file_image.ex. For now, everything in this file is commented out but we will provide some changes there in next section.

Local Storage

To store files locally in our project file system we will start with the setup of the storage provider in dev config:

config :waffle, storage: Waffle.Storage.Local

Now let's provide some changes to lib/file_uploader_web/uploaders/file_image.ex. Add use Waffle.Ecto.Definition at the top of the file which includes ecto support. We can also provide some file validation to our application. Let's assume we want uploaded files to have the following extensions: .jpg .jpeg .gif .png so let's add validation function to file_image.ex:

defmodule FileUploader.FileImage do
  use Waffle.Definition
  use Waffle.Ecto.Definition

  # Whitelist file extensions:
  def  validate({file, _}) do
    file_extension = file.file_name |> Path.extname() |> String.downcase()
    case  Enum.member?(~w(.jpg .jpeg .gif .png), file_extension) do
      true -> :ok
      false -> {:error, "invalid file type"}
    end
  end
end

Next part is to add the uploader to the Photo module in lib/file_uploader/gallery/photo.ex:

defmodule FileUploader.Gallery.Photo do
  use  Ecto.Schema
  use  Waffle.Ecto.Schema
  import  Ecto.Changeset

  schema "photos" do
    field :picture, FileUploader.FileImage.Type

    timestamps()
  end

  @doc false
  def changeset(photo, attrs) do
    photo
    |> cast_attachments(attrs, [:picture])
    |> validate_required([:picture])
  end
end

By default, our files are saved in /uploads folder. So now we need to configure the endpoint.ex to indicate that we will be serving static resources from the /uploads directory. Go to the lib/file_uploader_web/endpoint.ex and add a second static plug:

plug Plug.Static, at: "/uploads", from: Path.expand('./uploads'), gzip: false

We managed to configure everything on the backend side. Now we need to add some changes to the templates we generated earlier. We will start from lib/file_uploader_web/templates/photo/index.html.eex. We should change: <td><%= photo.picture %></td> to:

<td>
  <%= img_tag FileUploader.FileImage.url({photo.picture, photo}, signed: true)
  %>
</td>

The same change we need to provide in our show template in lib/file_uploader_web/templates/photo/show.html.eex. Let's change

<%= @photo.picture %>

to:

<%= img_tag FileUploader.FileImage.url({@photo.picture, @photo}, signed: true) %>

This change will allow us to display the photos correctly. We use the url/4 function which is injected into our FileImage module through use Waffle.Definition macro.

Okay, so now we can display our pictures, but we also need to make some changes when it comes to adding new images. Let's go to our form.html.eex template in lib/file_uploader_web/templates/photo/form.html.eex and change text input

<%= text_input f, :picture %>

to file input:

<%= file_input f, :picture %>

We also need to change our form into a multipart form. The form_for/4 function accepts a keyword list of options where we can specify this:

<%= form_for @changeset, @action, [multipart: true], fn f -> %>

Now let's check our changes and how what we've done works. You can run your local server and go to http://localhost:4000/photos, try to add a new photo and check if everything works fine.

Upload files to S3

If we want to upload files to Amazon S3 we need to add some extra libraries from ExAws to what we already have:

defp deps do
  [
    {:waffle, "~> 1.1.0"},

    # If using S3:
    {:ex_aws, "~> 2.1.2"},
    {:ex_aws_s3, "~> 2.0"},
    {:hackney, "~> 1.9"},
    {:sweet_xml, "~> 0.6"}
  ]
end

After that, it's time to set up some configurations. In this case, we are going to use Waffle.Storage.S3:

config :waffle,
  storage: Waffle.Storage.S3,
  bucket: "your_bucket"

and we must add ex_aws configuration:

config :ex_aws,
  json_codec: Jason
  # any configurations provided by https://github.com/ex-aws/ex_aws

Related posts

Dive deeper into this topic with these related posts

No items found.