HaskellYesod

Creating CSV files in Yesod

Posted on by

Comma Separated Values (CSV) files represent (in general) a very simple file format which has a strong usage when working with a (relatively) big amount of data. In this post, you will learn how to create such files in Yesod and return the file to the client.

First, as always, let's start with a hypothetical use case and create a simple model we would like to export and serve to the client consuming this response. We will assume we have a database full of orders of certain products:

OrderItem
  uuid Text
  productId ProductId
  customerId CustomerId
  title Text
  price Int
  discount Int
  amount Int
  comment Text Maybe
  orderedAt Day
  Primary uuid

Please, don't take this model as very elaborated. We just would like to have a "big" model with different data types so we can illustrate their usage during the export to CSV. We have different integer, string and date values we can convert to Text.

Usually, you would access this data in a way similar to this:

getOrderItemListR :: Handler Html
getOrderItemListR = do
  orderItems <- runDB $ selectList [] [Asc OrderItemOrderedAt]
  defaultLayout $ do
    setTitle "Shop orders"
    $(widgetFile "show/order-list")

This represents a very simple view of all orders inside a shop. As you can easily see we are returning Html here explicitly. Now, how can we create the same list but return this list of OrderItems as a CSV file? First, we need to convert the data into a CSV format. If we are able to achieve this, we need to return the generated contents as a file to the client.

Step 1: Transforming data

Even if this sounds simple to you, it's maybe not. In Haskell world, you will find many choices of libraries to solve a problem. Of course, CSV is not an exception. To transform your data, we will mention four options here: - The csv package - cassava - MissingH - Plain implementation

For simplicity, we will go with the plain implementation by just creating a list of lists of string (Text), like this:

transformOrderItems :: [Entity OrderItem] -> [Text]
transformOrderItems items =
  map (\itemEntity -> transformOrderItem $ entityVal item) items


transformOrderItem :: OrderItem -> Text
transformOrderItem orderItem =
  Text.concat [
  , orderItemTitle orderItem
  , ";"
  , Text.pack show $ orderItemAmount orderItem
  , ";"
  , Text.pack show $ orderItemProductId orderItem
  , ";"
  , Text.pack show $ orderItemOrderedAt orderItem
  , "\n"
  ]

Step 2: Returning data as a file

Please have a look at the simple implementation of a Handler function provided above. It's clear that we can not just return Html. For those cases Yesod has its TypedContent. Furthermore we will make use of the built-in addHeader function. The sendResponse function allows us to instantly send a response and define which content should be returned. Hence, we set the content as typePlain and just send our CSV formatted text to the client as a file.

getOrderItemCsvListR :: Handler TypedContent
getOrderItemCsvListR = do
  orderItems <- runDB $ selectList [] [Asc OrderItemOrderedAt]
  let filename :: Text = "export.csv"
  addHeader "Content-Disposition" $ Text.concat
        [ "attachment; filename=\"", filename, "\""]
  sendResponse (typePlain, toContent $ Text.concat (transformOrderItems orderItems))

Hopefully this blog post helps you to implement your own CSV export feature. If you have any questions or comments, please just use the area below.