A REST API you would not be embarrassed to hand over
A CRUD service is a morning's work. This is the rest of it: the error model, the pagination contract, the validation, and the twenty decisions that separate a demo from something another team can call.
The business problem
You are handing an API to a frontend team you will not sit next to. They will discover its behaviour from its responses, not from you.
Build a catalogue service — products and categories — where every failure mode is designed rather than defaulted, and every list endpoint answers the question a client actually has: what did I get, and is there more?
What you will have at the end
- One error shape for every failure, including the ones Spring throws before your code runs
- Pagination a client can page through without guessing
- Validation messages that come from the constraints, not from strings in a handler
- An OpenAPI document that is generated, not written
Milestones
Each one ends in something you can observe. Without that a milestone is a heading, and you have no way to know you finished.
The resource and its shape
GET/POST/PUT/DELETE over products. Separate request and response records — never expose the entity.
done whenA POST returns 201 with a Location header, and the response body has no JPA annotations behind it.
One error model
@RestControllerAdvice returning ProblemDetail. Handle MethodArgumentNotValidException, the 404 you throw yourself, and the catch-all — in that order.
done whenA malformed body, an unknown id and an unhandled exception all return the same JSON shape, and none of them contains a stack trace.
Pagination that is a contract
page/size with a hard maximum, a total count, and a documented default. Decide what page=9999 does and write it down.
done whensize=100000 does not return 100,000 rows, and the response says how many there are in total.
Filtering and sorting, bounded
Whitelist the sortable fields. An open sort parameter is a way to make the database scan whatever a caller names.
done whensort=password returns 400 rather than sorting by a column that exists.
The document
springdoc from the annotations. Examples on the error responses, not only the happy path.
done when/swagger-ui shows the 400 body a client will actually receive.
Data
Two tables and a foreign key. The interesting part is not the schema — it is that the API's shape is not the schema's shape, and you will feel the difference the first time you rename a column.