For many API applications, there comes a time when the application needs to save images uploaded to the server either locally or on a CDN. Luckily for us, Elixir and Phoenix provide the tools we need to build a simple image upload API.
The Simple API
Let’s define exactly how this API is supposed to work:
- accept a request containing a base64 encoded image as a field
- preserve the image extension by reading the image binary
- upload the image to Amazon’s S3
- provide the URL to the image on S3 in the response
Update Your Dependencies
To assist us with uploading images to S3, we will use ExAws to interact with the AWS API and UUID to help generate random IDs. Update your mix.exs
file to include both libraries as dependencies.
def deps do
[
...,
{:ex_aws, "~> 1.1"},
{:uuid, "~> 1.1"}
]
end
Also, make sure to update your application list if you’re using Elixir 1.3 or lower.
def application do
[
applications: [
...,
:ex_aws,
:hackney,
:poison,
:UUID
]
]
end
Lastly, include your AWS credentials in your config.exs
.
config :ex_aws,
access_key_id: ["ACCESS_KEY_ID", :instance_role],
secret_access_key: ["SECRET_ACCESS_KEY", :instance_role]
The AssetStore “Context”
Before we create the controller, let’s define the application logic in a separate module that is specific for handling uploaded assets. For our application, we are only going to support JPEG and PNG files. With a name like AssetStore
, we can add additional file types in the future but use the same context.
defmodule MyApp.AssetStore do
@moduledoc """
Responsible for accepting files and uploading them to an asset store.
"""
alias ExAws.S3
@doc """
Accepts a base64 encoded image and uploads it to S3.
## Examples
iex> upload_image(...)
"https://image_bucket.s3.amazonaws.com/dbaaee81609747ba82bea2453cc33b83.png"
"""
@spec upload_image(String.t) :: s3_url :: String.t
def upload_image(image_base64) do
image_bucket = "image_bucket"
# Decode the image
{:ok, image_binary} = Base.decode64(image_base64)
# Generate a unique filename
filename =
image_binary
|> image_extension()
|> unique_filename()
# Upload to S3
{:ok, response} =
S3.put_object(image_bucket, filename, image_binary)
|> ExAws.request()
# Generate the full URL to the newly uploaded image
"https://#{image_bucket}.s3.amazonaws.com/#{filename}"
end
# Generates a unique filename with a given extension
defp unique_filename(extension) do
UUID.uuid4(:hex) <> extension
end
# Helper functions to read the binary to determine the image extension
defp image_extension(<<0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, _::binary>>), do: ".png"
defp image_extension(<<0xff, 0xD8, _::binary>>), do: ".jpg"
end
Designing the Controller
Create a new controller responsible for images. We simply need to call our module that we previously made.
defmodule MyApp.ImageController do
use MyApp.Web, :controller
def create(conn, %{"image" => image_base64}) do
s3_url = MyApp.AssetStore.upload(image_base64)
conn
|> put_status(201)
|> json(%{"url" => s3_url})
end
end
Now let’s go update our router to include the new route in our API.
scope "/api", MyApp do
...
# Our new images route
resources "/images", ImageController, only: [:create]
end
Our application is now ready to accept images!
Try It Out
We can easily try out our new API by hitting up our terminal for a quick run-through with cURL. We can try uploading a 1x1 transparent PNG file.
curl -X "POST" "http://localhost:4000/api/images" \
-H "Content-Type: application/json" \
-d $'{
"image": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
}'
{"url": "https://image_bucket.s3.amazonaws.com/dbaaee81609747ba82bea2453cc33b83.png"}
Wrap Up
As we can see, Elixir and Phoenix provide the tools to add an API to accept base64 encoded image uploads with very little code. Be sure to read the docs of the dependencies we leveraged.