Inside the Labyrinth · the Koan Blog

Uploading Images to S3 from a React Single Page Application

Handling file uploads can be a tricky process. Here's how Koan's React SPA delivers files to AWS S3 without an extra trip through our servers.

Handling file uploads can be a tricky process. Backend API servers are usually best suited for receiving small bits of data from the client, reading to and writing from a database, and then returning other small bits of data. If your backend server is tasked with managing file uploads, you'll be saddling on the responsibility of being a complex mediator between your client and your file storage system, which in our case is [AWS S3](https://aws.amazon.com/s3/).

To avoid adding this additional complexity to our backend, we decided to allow our React [SPA](https://en.wikipedia.org/wiki/Single-page_application) client to upload directly to S3, using [pre-signed URLs](https://docs.aws.amazon.com/AmazonS3/latest/dev/PresignedUrlUploadObject.html). A pre-signed URL allows an untrusted party, i.e., anything other than our backend servers, temporary permission to read, modify, or delete a file that they would otherwise be unable to. This makes it great for handling file uploads inside [Koan](https://www.koan.co/), such as user avatars or company logos.

## The high-level flow

The procedure to get a file uploaded contains a few steps. Let's look at how a user might upload a new avatar image.

Workflow for Uploading Images to S3 from a React Single Page Application

1. The user navigates to a page in the SPA with a UI for setting an avatar image. At this point, the client requests a pre-signed upload URL from the server.
2. The server relays this request to S3.
3. S3 returns a new pre-signed URL to the server.
4. The server returns the pre-signed URL to the client.
5. The client displays an interface for the user allowing them to select a file for upload. The user selects a file from their computer, and the client uploads the selected file to the pre-signed URL (i.e., directly to S3).
6. The client sends a request to the server again, letting it know that an upload has completed. To keep persisted state down to a minimum, the server does not store the pre-signed URL it generates from Step 1, and instead requires the client to include the pre-signed URL from Step 1 in this request. The server writes the pre-signed URL to an `avatarUrl` field in the user model, persisting it to the database.

You may notice that we haven't fully removed our backend server from the equation. We have, however, reduced its role in the process from handling file uploads to passing around strings, a task it's much better suited for!

Let's dig into how this works in more detail.

## Generating pre-signed URLs on the server

On the server, we request a pre-signed URL from S3 via its [JavaScript SDK](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html) like so:

[https://gist.github.com/swac/42e3d02846e71fb9740612f3811e2f2e](https://gist.github.com/swac/42e3d02846e71fb9740612f3811e2f2e)

Note the `ContentType` and `Expires` fields above.

- The `ContentType` field allows us to constrain the file type that the pre-signed URL allows, so that users can't upload anything other than an image. If we wanted to constrain this further, to only allow GIF images, we'd say `ContentType: 'image/gif'`.
- The `Expires` field allows us to specify how many seconds a pre-signed URL should be usable for. Since we can generate a new pre-signed URL every time a user needs to upload an image, we use this field to ensure that these URLs don't stay active forever.

We require the caller to provide a directory name in addition to specifying the bucket so that our user uploads stay organized by use case (we allow users to upload personal avatars, company logos, and company banner images). An example usage of this function looks like:

[https://gist.github.com/swac/3a9e517992da6cb69e2d3c3cf736c301](https://gist.github.com/swac/3a9e517992da6cb69e2d3c3cf736c301)

In this example, `presignedUploadUrl` will be a URL inside the `koan-user-uploads` S3 Bucket, in the `companyLogos` directory. We expose this URL to the client by returning it at an endpoint like `/companies/:companyId/logo-upload-url`. Any client that uploads to this URL will automatically have their upload placed in this location.

## Wiring up the React side

So how do we make use of these URLs? Our client is a React SPA, so we opted to create an `ImgUpload` component that handles both the file selection (including drag-and-drop) and the process of upload the image to S3. What follows is a stripped down version of [custom hook](https://reactjs.org/docs/hooks-custom.html) for uploading to S3, ready for you to [copy and paste it right into your project](https://twitter.com/acdlite/status/1031014947025637381). To handle the lower-level details, we make use of the popular [`react-dropzone`](https://github.com/react-dropzone/react-dropzone) library.

[https://gist.github.com/swac/f53d766f1ad4d980fa1c5d7b52df1a4c](https://gist.github.com/swac/f53d766f1ad4d980fa1c5d7b52df1a4c)

Making use of this hook looks something like this:

[https://gist.github.com/swac/cb6eb940295e5e48052837da2dd7fe46](https://gist.github.com/swac/cb6eb940295e5e48052837da2dd7fe46)

Note that our hook requires its caller to provide it with the pre-signed URL via its `presignedUploadUrl` prop. For brevity, and since every application's server interaction is slightly different, we've omitted the scaffolding required for fetching this URL.

The `getRootProps` and `getInputProps` fields that we receive from the hook are provided directly from [`react-dropzone`](https://react-dropzone.js.org/), allowing our callers to get the full benefits of the library with our S3 uploading sprinkled in.

---

The approach we've outlined here should provide a starting point to getting image uploading working in any React SPA. Each application's needs are different, so be sure to consider exactly what your application's user experience should be when adding such a feature.

Ashwin Bhat
Ashwin Bhat