Skip to content

Returning CSV Content From an API in Spring Boot

Avoiding conversion overhead by returning CSV data directly

Importing data from CSV (Comma-Separated Values) files is a basic feature offered by many different services. At the same time, CSV is one of the most popular formats in data science.

Although dealing with CSV content is common, the approach I used to follow was to convert the data returned by an API or a query into a valid CSV file. This involves boilerplate code and unnecessary overhead.

So, producing CSV as output, allowing users to download it, is a more practical, advanced, and automatable solution.

Let’s see how to build an API returning CSV content in Spring Boot.

1. Adding the Required Dependencies

There are many libraries to deal with CSVs in Java, but I strongly recommend Apache Commons CSV. First of all, you need to add it to your project’s dependencies.

If you are a Gradle user, add this dependency to your project’s build file:

compile "org.apache.commons:commons-csv:1.8"

Otherwise, if you are a Maven user, add the following dependency to your project’s build POM:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-csv</artifactId>
    <version>1.8</version>
</dependency>

Now, you have all you need to produce or read CSV content in Spring Boot.

2. Defining the API

Before showing how to achieve the result, I want to delve into a few details. In the HTTP protocol, the Content-Type header is used to indicate the media type of the returned content.

As described here, the recommended media type for an HTTP request that returns CSV is text/csv. Please, note that the media types offered by Spring Boot through the MediaType class do not include it. Since you cannot use a predefined value proposed by the framework, you will have to custom define it.

Another vital notion to clarify is that CSV files are often very different from each other. You may need to produce files with different delimiters, enclosing quotes, escaping characters, line separators, encodings, etc. Even though this is not what this article is aimed at, you should know that Apache Commons CSV allows you to choose between multiple predefined formats or define your own through the CSVFormat class.

Now, let’s see what an API returning CSV in Spring Boot looks like:

Java

@GetMapping(value = "/exportCSV", produces = "text/csv")
public ResponseEntity<Resource> exportCSV() {
    // replace this with your header (if required)
    String[] csvHeader = {
            "name", "surname", "age"
    };

    // replace this with your data retrieving logic
    List<List<String>> csvBody = new ArrayList<>();
    csvBody.add(Arrays.asList("Patricia", "Williams", "25"));
    csvBody.add(Arrays.asList("John", "Smith", "44"));
    csvBody.add(Arrays.asList("Douglas", "Brown", "31"));

    ByteArrayInputStream byteArrayOutputStream;

    // closing resources by using a try with resources
    // https://www.baeldung.com/java-try-with-resources
    try (
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            // defining the CSV printer
            CSVPrinter csvPrinter = new CSVPrinter(
                    new PrintWriter(out),
                    // withHeader is optional
                    CSVFormat.DEFAULT.withHeader(csvHeader)
            );
    ) {
        // populating the CSV content
        for (List<String> record : csvBody)
            csvPrinter.printRecord(record);

        // writing the underlying stream
        csvPrinter.flush();

        byteArrayOutputStream = new ByteArrayInputStream(out.toByteArray());
    } catch (IOException e) {
        throw new RuntimeException(e.getMessage());
    }

    InputStreamResource fileInputStream = new InputStreamResource(byteArrayOutputStream);

    String csvFileName = "people.csv";

    // setting HTTP headers
    HttpHeaders headers = new HttpHeaders();
    headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + csvFileName);
    // defining the custom Content-Type
    headers.set(HttpHeaders.CONTENT_TYPE, "text/csv");

    return new ResponseEntity<>(
            fileInputStream,
            headers,
            HttpStatus.OK
    );
}

Kotlin

@GetMapping(value = ["/exportCSV"], produces = ["text/csv"])
fun exportCSV() : ResponseEntity<Resource> {
    // replace this with your header (if required)
    val csvHeader = arrayOf(
        "name", "surname", "age"
    )

    // replace this with your data retrieving logic
    val csvBody = ArrayList<List<String>>()
    csvBody.add(listOf("Patricia", "Williams", "25"))
    csvBody.add(listOf("John", "Smith", "44"))
    csvBody.add(listOf("Douglas", "Brown", "31"))

    val byteArrayOutputStream =
        ByteArrayOutputStream()
            .use { out ->
                // defining the CSV printer
                CSVPrinter(
                    PrintWriter(out),
                    // withHeader is optional
                    CSVFormat.DEFAULT.withHeader(*csvHeader)
                )
                    .use { csvPrinter ->
                        // populating the CSV content
                        csvBody.forEach { record ->
                            csvPrinter.printRecord(record)
                        }

                        // writing the underlying stream
                        csvPrinter.flush()

                        ByteArrayInputStream(out.toByteArray())
                    }
            }

    val fileInputStream = InputStreamResource(byteArrayOutputStream)

    val csvFileName = "people.csv"

    // setting HTTP headers
    val headers = HttpHeaders()
    headers.set(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=${csvFileName}")
    // defining the custom Content-Type
    headers.set(HttpHeaders.CONTENT_TYPE, "text/csv")

    return ResponseEntity(
        fileInputStream,
        headers,
        HttpStatus.OK
    )
}

First, a ByteArrayOutputStream object is initialized and passed to a CSVPrinter object. The latter is used to append the values of each record to the stream through the printRecord method. Then, the stream is written by calling the flush() method. Finally, the stream is wrapped in an InputStreamResource object and returned by the API using custom-defined HTTP headers.

Note that in the example I decided to add a header to the CSV by calling withHeader() while creating the CSVPrinter object. This is not mandatory and depends on what you want your output content to look like.

3. The API in Action

This is what an example of a response from such an API looks like:

name,surname,age
Patricia,Williams,25
John,Smith,44
Douglas,Brown,31

As you can see, the output generated by the API is valid CSV content that can be easily downloaded by users or passed to other systems.

Conclusion

Returning CSV content from an API is much more efficient than converting its output accordingly. Here we looked at how to achieve such a result in Spring Boot in Java or Kotlin.

I hope that you found this article helpful, thanks for reading!

nv-author-image

Antonello Zanini

I'm a software engineer, but I prefer to call myself a Technology Bishop. Spreading knowledge through writing is my mission.View Author posts

Want technical content like this in your blog?