Creating CSV files in Yesod
Posted on Feb 22, 2017 by Alexej Bondarenko
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.