<- Back to blog

Using Active Storage Direct Uploads in GraphQL with Ruby on Rails

How we use Active Storage's direct upload feature for seamless file uploading in a GraphQL-powered Ruby on Rails application.

Vivek Patel

Vivek Patel

We're using GraphQL a lot, and due to it's reusability nature, we often pair it with React.js and React Native applications.

However, one challenge we faced was file uploads. GraphQL doesn't come with a built-in way to handle file uploads. While solutions like Apollo Upload Server exist, but we wanted to explore Active Storage's direct upload as an official solution.

What is Direct Upload?

In a normal (non-direct) file upload, the file is first sent to the Rails server, which then processes and uploads it to cloud storage (such as S3, Azure, Google cloud storage).

In Direct Upload, files go straight from the browser to cloud storage, which improves performances and reduces server load.

Direct upload is not limited only to the cloud storages mentioned above, You can even upload to server's local disk, in order to keep things simple.

In this guide, we'll be using React.js, Ant Design, Ruby GraphQL and direct upload to rails's local disk.

1. Integrate direct upload with form component

Rails has a built-in endpoint for direct uploads at /rails/active_storage/direct_uploads. You can check out the controller source code here.

Let's define a helper function for file uploads:

// src/api/activeStorage.ts
import { Blob, DirectUpload } from "@rails/activestorage";

const BASE_URL = "https://example.com/rails/active_storage/direct_uploads";

const directUploadCreate = (file: File) => {
  const upload = new DirectUpload(file, BASE_URL);

  return new Promise<Blob>((resolve, reject) => {
    upload.create((err, blob) => {
      if (err) {
        reject(err.message);
      } else {
        resolve(blob);
      }
    });
  });
};

When this function is called with a selected file, it returns a signed_id string, which acts as a placeholder for the uploaded file. Since it's just a string, calling the mutation becomes much simpler compared to using ApolloUploadServer's file type.

You can now treat avatar like a regular string field and update your GraphQL schema accordingly to accept it as a string.

Now, let's integrate above function with Ant Design's form component:

// src/pages/user-form.tsx
const UserForm = () => {
  return (
    ...
    <Form.Item>
      <Upload
        customRequest={async ({ file }) => {
          form.setFieldsValue({
            avatar: (await directUploadCreate(file)).signed_id
          });
        }}
      >
        <Button>Upload</Button>
      </Upload>
    </Form.Item>
    ...
  );
};

2. Update GraphQL mutation

Update GraphQL schema to accept avatar as a string field:

# app/graphql/types/user_attributes.rb
class Types::UserAttributes < Types::BaseInputObject
  argument :avatar, String, required: false
  # ...
end

Create user update mutation:

# app/graphql/mutations/user_update.rb
class Mutations::UserUpdate < Mutations::BaseMutation
  argument :attributes, Types::UserAttributes, required: true

  type Types::UserType, null: false

  def resolve(attributes:)
    user = context[:current_user]

    if !user.update(**attributes)
      raise GraphQL::ExecutionError, user.errors.full_messages.to_sentence
    end

    user
  end
end

Add mutation to schema:

# app/graphql/types/mutation_type.rb
class Types::MutationType < Types::BaseObject
  field :user_update, mutation: Mutations::UserUpdate, null: false
end

3. Call mutation from client

To trigger GraphQL mutation on form submission, we'll use graffle to send the request from our React app:

// src/api.ts
import { gql } from "graffle";

export const userUpdateDocument = gql`
  mutation userUpdate($input: UserUpdateInput!) {
    userUpdate(input: $input) {
      avatar
      id
    }
  }
`;

This is how complete integration looks:

// src/pages/user-form.tsx
import { request } from "graffle";

const UserForm = () => {
  const [form] = Form.useForm();

  return (
    <Form
      form={form}
      onFinish={(attributes) => {
        request(userUpdateDocument, { input: { attributes } });
      }}
    >
      <Form.Item>
        <Upload
          customRequest={async ({ file }) => {
            form.setFieldsValue({
              avatar: (await directUploadCreate(file)).signed_id,
            });
          }}
        >
          <Button>Upload</Button>
        </Upload>
      </Form.Item>
    </Form>
  );
};

That's it!

If you found this post useful, don't miss our latest tech posts. Keep innovating!

Have a project that needs help? Get in touch with us today!

Schedule a free consultation with our experts. Let's build, scale, and optimize your web application to achieve your business goals.