Create projects with the default board tags
Java · Spring Boot · beginner · greenfield
Adds project creation to the API. `ProjectController` exposes `POST /api/projects` for the signed-in user's workspace and validates the body through the new `CreateProjectRequest` record (name and tag limits as Bean Validation constraints). `ProjectService.create` starts from the board's default tags, appends the user's tags normalised to lower case and skips the ones already present, saves the project and returns its view.
Project-tracking backend on Spring Boot 3.3 (Java 21), Spring Data JPA (Hibernate 6) on PostgreSQL, Spring Security. Every `/api/**` request needs a signed-in session and the existing `SecurityConfig` handles CSRF; `@AuthenticationPrincipal` resolves a `WorkspaceUser(Long userId, Long workspaceId)` record whose fields are never null. An existing request filter answers 400 to any request whose body or decoded JSON strings contain NUL or other C0 control characters or an unpaired UTF-16 surrogate (raw or as `\u` escapes) before it reaches a controller. Existing and unchanged: the `Project` entity (`id`, `workspaceId`, `name` as `varchar(100)`, and `tags` as an ordered `@ElementCollection` of `varchar(30)` in `project_tags`), `ProjectRepository extends JpaRepository<Project, Long>`, and the `ProjectView` record with `ProjectView.from(project)`, which copies the tags. `spring-boot-starter-validation` is on the classpath, and Spring answers a failed `@Valid @RequestBody` with 400.
Requirements
- `POST /api/projects` with a JSON body `{"name": ..., "tags": [...]}` creates a project in the signed-in user's workspace and answers 201 with a `ProjectView` (id, name, tags). The name is stored trimmed.
- Every new project starts with the default tags `backlog`, `in-progress` and `done`, in that order. Tags sent in the request are appended after the defaults, in request order, each trimmed and lower-cased. A tag that is already in the list (a default, or an earlier tag in the same request) is skipped, not an error. When `tags` is missing, `null` or empty, the project gets only the defaults.
- Input limits, answered with 400 when broken: `name` is required, at most 100 UTF-16 code units as sent (what `@Size` counts) and not blank as `@NotBlank` checks it (empty after `String.trim()`); `tags` has at most 10 elements; each element is non-null, at most 40 characters as sent and, after trimming, 1 to 30 characters from `A-Z`, `a-z`, `0-9` and `-`.
Files touched
- src/main/java/com/acme/projects/projects/CreateProjectRequest.java
- src/main/java/com/acme/projects/projects/ProjectController.java
- src/main/java/com/acme/projects/projects/ProjectService.java
--- src/main/java/com/acme/projects/projects/CreateProjectRequest.java
+package com.acme.projects.projects;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
+import java.util.List;
+
+public record CreateProjectRequest(
+ @NotBlank @Size(max = 100) String name,
+ @Size(max = 10) List<@NotNull @Size(max = 40) @Pattern(regexp = "\\s*[A-Za-z0-9-]{1,30}\\s*") String> tags) {
+}
+
--- src/main/java/com/acme/projects/projects/ProjectController.java
+package com.acme.projects.projects;
+
+import com.acme.projects.security.WorkspaceUser;
+import jakarta.validation.Valid;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseStatus;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/projects")
+public class ProjectController {