profile image
Sean Walker

Vanilla js direct upload to S3

I couldn’t find a great guide for getting s3 direct uploadings working with vanilla js and fetch so here is one

I use shrine for the presigning but anything will work.

S3 setup

Don’t forget to set the CORS stuff and any bucket settings you want for your bucket, make sure to allow PutObject at least.

Here’s my CORS config:

        "AllowedHeaders": [
        "AllowedMethods": [
        "AllowedOrigins": [
        "ExposeHeaders": [
        "MaxAgeSeconds": 3000


Here’s the html, it’s just a form with a file upload

<form method="POST">
  <input type="file" />


Here’s the good part, the whole point of this post. It’s not the best code but it does work:

const s3 = {
  upload: function(file, data) {
    let formData = new FormData();

    const keys = Object.keys(data.fields);
    keys.forEach(k => {
      formData.append(k, data.fields[k]);
    formData.append("file", file);

    return fetch(data.url, {
      method: data.method,
      body: formData
    .then(res => console.log(res))
    .catch(error => console.log(error))

  presignedUrl: function(file) {
    return fetch('/s3-presigned-url', {
      method: 'post',
      headers: {
        "Content-Type": "application/json"
      body: JSON.stringify({
        contentType: file.type
    .then(response => response.json())
    .then(data => data);

  uploadFrom: function(selector) {
    const input = document.querySelector(selector);
    input.addEventListener('change', (e) => {
      const file =[0]

          .then(data => {
            this.upload(file, data)


This is no frills, no upload indicator, no pause/resume. Nothing, just straight upload to s3.

I’ll show you the backend but it’s not really interesting unless you’re using shinerb:


Here’s the shrine config:

require "shrine"
require "shrine/storage/s3"

s3_options = {
  bucket:            ENV["AWS_S3_BUCKET"],
  access_key_id:     ENV["AWS_ACCESS_KEY_ID"],
  secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
  region:            ENV["AWS_REGION"]

Shrine.storages = {
  cache: "cache", **s3_options),

Shrine.plugin :sequel
Shrine.plugin :cached_attachment_data
Shrine.plugin :restore_cached_data
Shrine.plugin :rack_file

And here’s the hilariously short presign code:

is "s3-presigned-url" do
  Shrine.storages[:store].presign(params["filename"], method: :post, content_type: params["contentType"])

Hopefully this helps someone out there!

Oh and if it does help you, buy me a coffee! No I’m kidding.