How to Export to CSV with Ruby on Rails
Tags: ruby, rails, webdev
Published: 2021-11-04 16:41:08

So, you need to export a CSV.

As with everything in Ruby on Rails, it's really easy to do this. 

I will walk through how I built this feature to export sales leads on another Rails app of mine. But this same approach can be applied to any data you want to export as a CSV file.

We are going to build an endpoint that a user can navigate to, that will trigger a download of the CSV file.

The idea is, they land on a route like this `https://example.com/export/leads.csv` and we start a download instead of rendering a normal page.

## Create the Route

First we need to create a route that we point the user to.

Add something like this to your `config/routes.rb` file:

```ruby
get 'leads/export', to: 'leads#export'
```

Change the name of your controller to whatever you want. Same with the method name. I went with `export` because that makes it clear what this method is responsible for.

## Create the Controller

Rails will expect the leads controller to be defined at `app/controllers/leads_controller.rb`, let's create that.

Initially, the contents of this controller should look something like this.

```ruby
require 'csv'

class LeadsController < ApplicationController
  def export
    @leads = Leads.where(organization_id: current_user.organization_id)

    respond_to do |format|
      format.csv do
        response.headers['Content-Type'] = 'text/csv'
        response.headers['Content-Disposition'] = "attachment; filename=leads.csv"
      end
    end
  end
end
```

We need our `export` method to do a few things here.

- pull data from the database
- convert our data into a CSV file
- format the response so we download the CSV

First let's pull the data. We'll need to build a query and save the results to an instance variable.

```ruby
@leads = Leads.where(organization_id: current_user.organization_id)
```

Next we have to convert the result of this query into the CSV format.

At the top of our controller let's import the built-in Rails `csv` library:

```ruby
require 'csv'
```

And now we can format our controller to respond when CSV is requested. Rails uses the `respond_to` method to allow controller endpoints to respond to multiple formats. You will see a lot of endpoints that respond with HTML, JSON, and even XML.

Here's what those formats would normally look like.

```ruby
respond_to do |format|
    format.html
    format.json { render json: @leads.to_json }
    format.xml { render xml: @leads.to_xml }
  end
```

We'll need to slightly modify this to return CSV data.

Our version will instead respond to `format.csv`:

```ruby
respond_to do |format|
  format.csv do
    response.headers['Content-Type'] = 'text/csv'
    response.headers['Content-Disposition'] = "attachment; filename=leads.csv"
    render template: "path/to/index.csv.erb"
  end
end
```

This means when someone makes a request to our URL, which was `https://example.com/export/leads.csv`, the tailing `leads.csv` extension tells our server to respond with the csv format.

Also, these <a href="https://testsuite.io/content-type-header" target="_blank">Content-Type headers</a> tell the browser that we are returning a CSV file to download.

## Building the CSV File

Our backend is fully wired up to export CSV for the user.

Now we need to generate the actual CSV file. The controller will automatically render the template found at `app/views/leads/export.csv.erb`. 

Let's create that, and then here's what it will look like:

```erb
<%- headers = ['Email', 'Date'] -%>
<%= CSV.generate_line headers %>
<%- @leads.each do |lead| -%>
  <%= CSV.generate_line([lead.data['email'], lead.created_at]) -%>
<%- end -%>
```

Formatting a CSV file is very straightforward in Rails. We just need to call `CSV.generate_line` to add a row.

First we declare a variable called `headers`, and store the headers of our CSV file.

```erb
<%- headers = ['Email', 'Date'] -%>
```

Next we generate the first row for these headers with this call:

```erb
<%= CSV.generate_line headers %>
```

And last, we loop through our query result and again generate a row for each lead:

```erb
<%- @leads.each do |lead| -%>
  <%= CSV.generate_line([lead.data['email'], lead.created_at]) -%>
<%- end -%>
```

And that's it. 

Now we have an endpoint that a user can access to download their data as a CSV. You can point users to this endpoint with a button and the download should begin automatically.

Something like this should do the trick!

```erb
<a href="/leads/export.csv">Export CSV</a>
```

Of course, replace the query I used and pull whatever records you want in your controller.

Pass `CSV.generate_line()` as many arguments as you want columns, and don't forget to pass headers in the first line to match!