The generated API and error answers
The API of an @expose entity
Base path /api/<domain>/<entity> - the entity name with a small first letter (PurchaseOrder is
/api/shop/purchaseOrder); apiPath in env.yml sets /api.
| Request | Does |
|---|---|
POST / with the fields | create; answers the row with its key |
GET /{key} | read one; 404 when there is none |
PUT / with the key in the body | update (an @editable root or field) |
DELETE /{key} | delete; answers true |
POST /search {field: value, "pageNumber": 0, "pageSize": 20, "sortCode": "title+"} | a page: content, totalElements, totalPages, number |
PUT /batch with a list | update several in one transaction - each row goes through update(dto) |
POST /delete with a list of keys | delete several; a list of true/false |
GET /schema/info | the entity's field descriptions, for a generic screen |
POST /{key}/<command> | a lifecycle step or command (DDD bundles) |
POST /signin, POST /signout, GET /me | with @signin |
There is no GET for a list. The controller documents every operation for springdoc
(/swagger-ui.html, /v3/api-docs).
@restExclude(...)
Leaves methods out of the service, controller, use case and TypeScript client. Names: the service's
save, update, batchUpdate, delete (a list), deleteById, updateBySpecification, and the
controller's create, read, search, update, batchUpdate, deleteBatch (POST /delete),
deleteById, schemaInfo. On v3 what calls a removed method goes with it, so it still compiles:
update takes batchUpdate, deleteById takes delete and deleteBatch, delete takes
deleteBatch, save takes create. A Kotlin service keeps its methods; its controller and use case
follow. The 2.x-compatible output removes exactly what is written.
What the v3 write path checks
The core services of jpa-springboot-v3, ddd-springboot, hexagonal-springboot and the Kotlin
editions check before writing (the 2.x-compatible jpa-springboot does not change):
| Case | Behaviour | Answer |
|---|---|---|
| a create with a key that exists | refused, not overwritten | 409 |
an update whose version differs from the row's | refused (optimistic lock) | 409 |
an update value that breaks a constraint (@Size ...) | refused - only the fields sent are checked | 400 |
| an update of a row that does not exist | EntityNotFoundException | 404 |
| a key of another aggregate that names no row (DDD) | reference.<Entity>.<relation> | 404 Member 99 does not exist |
a command or delete the state does not allow (states:) | state.... | 409 |
| another invariant broken | InvariantViolationException | 422 |
| a rule you wrote refuses | the runtime's exceptions, below | 400 / 401 / 403 / 404 / 409 / 502 |
The DDD mappers also never write @jpa:Version or the Spring Data audit fields from a request: they
are answered, so a client sends the version back, but a request cannot forge the audit record.
Error answers: application/problem+json
Every v3 bundle generates, per domain with exposed entities, one XxxApiErrors
(@RestControllerAdvice, RFC 9457 ProblemDetail) in the controller package - generate-once, so
the answer's shape is yours. It also imports the runtime's RequestShapeErrors, which answers what
Spring MVC refuses before a controller is called, in the same shape, for the whole application.
HTTP/1.1 409
Content-Type: application/problem+json
{ "title": "Not allowed in this state",
"detail": "PurchaseOrder cannot cancel when status is SHIPPED; it must be [PLACED]",
"code": "state.PurchaseOrder.status.cancel", "status": 409 }
A 400 for fields also has errors ("field: message") and fieldErrors:
{ "status": 400, "code": "invalid",
"fieldErrors": [{ "field": "lines", "code": "Size", "message": "the cart is empty" }] }
Before the controller (RequestShapeErrors)
| Request | Status | code |
|---|---|---|
| no body, or not JSON | 400 | unreadable |
a value its type refuses - letters for a number, an unknown enum code, a negative quantity | 400 | invalid, fieldErrors[].code typeMismatch (lines[0].quantity: a quantity cannot be negative: -1) |
| a required query parameter missing | 400 | invalid, NotNull |
| a path that does not exist | 404 | not-found |
a method the path does not have (PUT on a root that is not @editable) | 405 (with Allow) | method-not-allowed |
an answer format it cannot give (Accept: image/png) | 406 | not-acceptable |
a body format it cannot read (Content-Type: text/plain) | 415 | unsupported-media-type |
A browser asking for a page (Accept: text/html) still gets Spring Boot's error page for 404 and 405.
Do not set spring.mvc.problemdetails.enabled=true: Spring Boot's handler would take the field
validation answers. It needs Spring Framework 6.1 (Spring Boot 3.2) or later.
Codes
| Code | When |
|---|---|
not-found, already-exists, stale, required, unique, referenced, conflict, invalid, refused, state, not-signed-in, forbidden, bad-request, no-such-property | shared by all domains |
state.<Entity>.<field>.<command> | a lifecycle step from the wrong state (409) |
invariant.<Entity>.<field>.min / .max / .minsize / .maxsize | a field's range invariant |
invariant.<Entity>.<condition> | an @invariant (items.size > 0 gives items-size-gt-0) |
reference.<Entity>.<relation> | a referenced row that does not exist (404) |
upstream | another system failed (PortCallException, 502) |
unreadable, method-not-allowed, unsupported-media-type, not-acceptable | refused before the controller |
The TypeScript client's ApiError reads code and fieldErrors (TypeScript).
Refusals your code throws
Exception (io.elasticore.springboot3.) | Status | code without a code of yours |
|---|---|---|
validation.BadRequestException(code, message, args...) | 400 | bad-request |
security.NotSignedInException(code, message, args...) | 401 | not-signed-in |
security.ForbiddenException(code, message, args...) | 403 | forbidden |
validation.NotFoundException(code, message, args...) | 404 | not-found |
validation.ConflictException(code, message, args...) | 409 | conflict |
http.PortCallException(code, message, args...) | 502 | upstream |
validation.InvariantViolationException(code, message, args...) | state. 409, reference. 404, otherwise 422 |
The message is the detail; with a message code it is translated into the request's language (see
Messages and languages). new ConflictException("...") without a code works too.
throw new ConflictException("loan.no-copy-left", "No copy of {0} is left to lend", isbn);
HTTP/1.1 409
{ "title": "Conflicts with stored data", "detail": "No copy of 978-0-00 is left to lend",
"code": "loan.no-copy-left", "status": 409 }
XxxApiErrors you editedIt is generate-once: one you never edited is regenerated with each change (REGENERATED(owned, never edited)); one you edited stays as it is. From 3.0.261022-rc1 a refused lifecycle step is 409 and a
missing reference 404 (before: 422 and a foreign-key 409). An edited file still answers the old way -
change its refused(...) as a new one does (state. 409, reference. 404), add
@Import(io.elasticore.springboot3.validation.RequestShapeErrors.class), or delete it and build again.
Upgrade the generator and the runtime library to the same version.