Let project members download a project as JSON

Java · Spring Boot · intermediate · modification

Adds a JSON export of a project for the upcoming Export button. `GET /api/project-exports/{projectId}` returns the project's name and all of its tasks in id order as a download named `project-<id>.json`. `ProjectExportService` loads the project (404 when it does not exist) and its tasks in one read-only transaction, and `TaskRepository` gains the ordered finder. `WebConfig` and `ProjectMembershipInterceptor` are shown for reference and are unchanged.

Task-tracker backend on Spring Boot 3.3 (Java 21), Spring MVC with the default `PathPatternParser`, Spring Data JPA (Hibernate 6) on PostgreSQL, Spring Security with session login. Every `/api/**` request needs a signed-in session (anonymous requests get 401 before reaching MVC); the principal is a `TaskUser(Long userId, String email)` record and `userId` is never null. Project-scoped endpoints rely on `ProjectMembershipInterceptor` as registered in `WebConfig` (both shown, unchanged); controllers do not repeat the membership check. Existing and unchanged: the `Project` (`id`, `name`), `Task` (`id`, `projectId`, `title`, `status` as a `TaskStatus` enum stored by name, nullable `dueDate`) and `Membership` (`projectId`, `userId`) entities, `ProjectRepository extends JpaRepository<Project, Long>`, and `MembershipRepository.existsByProjectIdAndUserId`. Project ids are sequential `bigserial` values. Jackson writes `LocalDate` as `yyyy-MM-dd` (Spring Boot's default).

Requirements

Files touched

--- src/main/java/com/acme/tasks/exports/ProjectExportController.java
+package com.acme.tasks.exports;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class ProjectExportController {
+
+    private final ProjectExportService exports;
+
+    public ProjectExportController(ProjectExportService exports) {
+        this.exports = exports;
+    }
+
+    @GetMapping("/api/project-exports/{projectId}")
+    public ResponseEntity<ProjectExport> export(@PathVariable Long projectId) {
+        ProjectExport body = exports.export(projectId);
+        return ResponseEntity.ok()
+                .header(HttpHeaders.CONTENT_DISPOSITION,
+                        "attachment; filename=\"project-" + body.id() + ".json\"")
+                .body(body);
+    }
+}
+
--- src/main/java/com/acme/tasks/exports/ProjectExportService.java
+package com.acme.tasks.exports;

Review this PR

Java practice