java 53 lines · 9 steps

Streaming a CSV export in Spring

A Spring endpoint streams orders straight to the HTTP response as CSV without buffering them all in memory.

Explained by highlit
1@RestController
2@RequestMapping("/api/reports")
3public class OrderReportController {
4 
5 private final OrderRepository orderRepository;
6 
7 public OrderReportController(OrderRepository orderRepository) {
8 this.orderRepository = orderRepository;
9 }
10 
11 @GetMapping("/orders")
12 public ResponseEntity<StreamingResponseBody> streamOrders(
13 @RequestParam(defaultValue = "#{T(java.time.LocalDate).now().minusMonths(1)}") LocalDate from,
14 @RequestParam(defaultValue = "#{T(java.time.LocalDate).now()}") LocalDate to) {
15 
16 StreamingResponseBody body = out -> {
17 var writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));
18 writer.write("id,customer,placed_at,status,total\n");
19 
20 try (Stream<Order> orders = orderRepository.streamByPlacedAtBetween(from.atStartOfDay(), to.atTime(LocalTime.MAX))) {
21 orders.forEach(order -> {
22 try {
23 writer.write(String.join(",",
24 Long.toString(order.getId()),
25 escape(order.getCustomerName()),
26 order.getPlacedAt().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME),
27 order.getStatus().name(),
28 order.getTotal().toPlainString()));
29 writer.write('\n');
30 } catch (IOException e) {
31 throw new UncheckedIOException(e);
32 }
33 });
34 }
35 writer.flush();
36 };
37 
38 String filename = "orders-%s-to-%s.csv".formatted(from, to);
39 return ResponseEntity.ok()
40 .contentType(new MediaType("text", "csv", StandardCharsets.UTF_8))
41 .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
42 .cacheControl(CacheControl.noCache())
43 .body(body);
44 }
45 
46 private static String escape(String value) {
47 if (value == null) return "";
48 if (value.contains(",") || value.contains("\"") || value.contains("\n")) {
49 return '"' + value.replace("\"", "\"\"") + '"';
50 }
51 return value;
52 }
53}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Returning a StreamingResponseBody lets you write to the response as data arrives instead of materializing a whole payload.
  2. 2Pairing a JPA Stream inside a try-with-resources keeps the result set open only for the duration of the write.
  3. 3Setting Content-Disposition and a specific media type turns a plain endpoint into a browser-triggered file download.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Streaming a CSV export in Spring — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code