Using Active Storage in Ruby on Rails Api

Tor Francis - Feb 16 '23 - - Dev Community

Hi, if your building a project like I am and want to use Active Storage to handle file upload then you've come to the right place.

First step is to setup your environment by creating a new rails application. I used postgresql for my database.

Now you will want to create and migrate your model and controller for whatever table you want to have files for. Let's use User as our model.

install the Active Storage gem

rails active_storage:install
Enter fullscreen mode Exit fullscreen mode

This will create a migration will 3 tables: active storage blobs, active storage attachments, active storage variant records.

create your schema by running:

rails db:migrate
Enter fullscreen mode Exit fullscreen mode

Now you will want to create an association(macro) to your User table.

class User < ApplicationRecord
  has_one_attached :image
end
Enter fullscreen mode Exit fullscreen mode

We are going to make the attachment an Image, you can name it whatever kind of file it's going to be, we will reference it in our UsersController.

Next, you will want to create the params for the User in the UsersController.

def user_params
  params.require(:user).permit(:name, :image)
end
Enter fullscreen mode Exit fullscreen mode

In your gem , you'll want to uncomment out

# gem 'rack-cors' 
Enter fullscreen mode Exit fullscreen mode

and run a bundle install. This will add it to your gemfile.lock

rack-cors (1.1.1)
rack (>= 2.0.0)
Enter fullscreen mode Exit fullscreen mode

Go to config/initializers/cors.rb and you'll see a new file added:


# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

# Rails.application.config.middleware.insert_before 0, Rack::Cors do
#   allow do
#     origins 'example.com'
#
#     resource '*',
#       headers: :any,
#       methods: [:get, :post, :put, :patch, :delete, :options, :head]
#   end
# end
Enter fullscreen mode Exit fullscreen mode

Uncomment out everything after 'Read more: https://github.com/cyu/rack-cors'. If your using a react app you'll want to replace origins will your react app server

 Rails.application.config.middleware.insert_before 0, Rack::Cors do
   allow do
     origins 'example.com'
Enter fullscreen mode Exit fullscreen mode
 Rails.application.config.middleware.insert_before 0, Rack::Cors do
   allow do
     origins 'http://localhost:3000'
Enter fullscreen mode Exit fullscreen mode

Make sure you input the correct server you using or you will get an error.

Rails.application.routes.draw do
resources :users, only: [:create]
end
Enter fullscreen mode Exit fullscreen mode
class UsersController < ApplicationController
  def create
        user = User.create(user_params)
        if user.valid?
            user.save
            render json: user, status: :created
        else
            render json: { errors: ['User not valid']}, status: 422
        end
    end
end
Enter fullscreen mode Exit fullscreen mode

On your Frontend you'll want to create an form that that send a POST to your backend route. I'm using Material UI for styling.

I created a SignUpForm component for the form and useState for our Input fields. For our file input field we specify the input type for our file. In Material UI, we use inputProps that.

We add an on change function that uses an anonymous function that take in an event(e) and calls the setter function from our useState.

import React, { useState } from "react";
import { TextField, Input, Typography, Button } from "@mui/material";

Function SignupForm(){
const [image, setImage] = useState([])
[name, setName] = useState("")


return (
 <form>
   <Typography> Name </Typography>
        <TextField
          label="Name"
          value={name}
          onChange={(event) => 
          setName(event.target.value)}

        />
        <br />
        <Input
          label="Image"
          type="file"          
          inputProps={{ accept: "image/*" }}
          onChange={(event) => 
          setImage(event.target.files[0])}
          name="image"
        />

        <br />
        <Button color="primary" type="submit">
          Submit
        </Button>
 </form>
)
}
export default SignUpForm
Enter fullscreen mode Exit fullscreen mode

Notice that our onChange for Name and Image are different. When were selecting a file, we can typically select multiple files. We use "event.target.files[0]" because the files are in an array and we just want the first so we add an index of 0. This means we also have to set our initial state of image to an array.

We will add a function to the form that sends a POST request to our end point in our Backend. Because we have a file we have to send, we can't send a normal object. Instead We'll send a FormData object that we'll construct in the function.

function handleSubmit(e) {
    e.preventDefault();
    const data = new FormData();
    data.append("user[image]", image);    
    data.append("user[name]", name);


    fetch("http://localhost:3000/users", {
      method: "POST",
      body: data,
    })
}
Enter fullscreen mode Exit fullscreen mode

We structure our FormData object in a manner that ruby accepts and send it to the backend without stringifying it. Add the handle submit to the form and test it out.

If open up the console, you will see that the new user has an image at attached.

Thank you!

. . . . . . .