Response
I'm assuming `ApiResponse` lives in `com.acme.billing`. If it lives somewhere else, only the import changes. Per the conventions, `@Valid` on the body triggers validation, and the advice handler maps validation failures.
**`InvoiceDtos.java`**
```java
package com.acme.billing;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
public final class InvoiceDtos {
private InvoiceDtos() {
}
public record CreateInvoiceRequest(
@NotNull @Positive Long customerId,
@NotNull @Min(1) Long amountCents,
@Size(max = 200) String note) {
}
public record InvoiceResponse(
long id,
long customerId,
long amountCents,
String note) {
}
}
```
**`InvoiceController.java`**
```java
package com.acme.billing;
import com.acme.billing.InvoiceDtos.CreateInvoiceRequest;
import com.acme.billing.InvoiceDtos.InvoiceResponse;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
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/billing")
public class InvoiceController {
private final InvoiceService invoiceService;
public InvoiceController(InvoiceService invoiceService) {
this.invoiceService = invoiceService;
}
@PostMapping("/invoices")
@ResponseStatus(HttpStatus.CREATED)
public ApiResponse<InvoiceResponse> create(@Valid @RequestBody CreateInvoiceRequest request) {
InvoiceResponse invoice = invoiceService.create(
request.customerId(), request.amountCents(), request.note());
return ApiResponse.ok(invoice);
}
}
```
This assumes `InvoiceService.create` returns `InvoiceDtos.InvoiceResponse`. If it returns another record, map it to `InvoiceResponse` in the controller, or change the return type.
Conventions applied: the DTOs are records in an `InvoiceDtos` holder, with `jakarta.validation` on the request. The controller uses constructor injection, has no try/catch, and wraps the result in `ApiResponse.ok`.
The request fields are boxed `Long` so a missing value fails `@NotNull` instead of silently becoming 0. `note` is optional, so it only has `@Size(max = 200)`. The endpoint returns 201 Created.