APIsFoundation

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

Trade-offs you will have to defend

Offset pagination is simple and gets slower the deeper you go. Keyset pagination is fast and cannot jump to page 40. Pick one and be able to say why.
ProblemDetail (RFC 9457) or your own envelope. Either is defensible; having both in one API is not.
Returning the created object versus returning only a Location. The second is smaller and costs the client a round trip.