I am using OpenAPI 3.0 to design and implement a simple API to allow basic CRUD operations on database entities.
Let's assume an entity Pet
with some client-given properties (name
& age
) and some generated properties (id
& created
):
components:
schemas:
Pet:
type: object
properties:
id:
type: string
created:
type: string
format: date-time
name:
type: string
age:
type: integer
I want to specify REST endpoints to POST, GET, PUT (and if possible PATCH) a pet:
paths:
/pets:
post:
operationId: createPet
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
responses:
"200":
content:
application/json:
schema:
type: boolean
/pets/{id}:
parameters:
- name: id
schema:
type: string
in: path
required: true
get:
operationId: getPet
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
put:
operationId: updatePet
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
responses:
"200":
content:
application/json:
schema:
type: boolean
patch:
operationId: alterPet
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Pet"
responses:
"200":
content:
application/json:
schema:
type: boolean
From a logical point of view, the following properties are required per endpoint:
- GET:
id
,created
,name
,age
- POST:
name
,age
- PUT:
name
,age
- PATCH:
name
orage
(or none depending on implementation)
I see two major approaches here:
Approach 1: Leave all properties optional
The Pet
DTO is used as a shell where all properties are optional as defined above. It is left to the server & client to check if the required properties are filled on an endpoint call. If non-required properties are set in a POST/PUT/PATCH request body, they will be ignored.
pros:
- Single, simple, and symmetric DTO schema definition in the API specification.
- Smooth integration of the "GET -> modify -> PUT" workflow on the client side (e.g., a React frontend with Formik).
- Supports the PATCH endpoint out-of-the-box.
cons:
- All properties are optional and it is very tedious to handle these
string | undefined
types in Typescript. This is especially annoying in the GET direction as we know that all properties will be filled as long as we retrieve thePet
DTO. - The implementational "pain" is spread over the server and all possible clients.
Approach 2: Define separate DTO schemas for all operations
We introduce a GetPet
, PutPet
, PostPet
, and PatchPet
with corresponging required
lists in the API specification. Yes, PutPet
and PostPet
can be identical, but maybe we want to allow a modified GetPet
to be used as PutPet
to streamline the "GET -> modify -> PUT" workflow for the client.
Imagine the Pet
entity having 5+ generated properties and 20+ client-given properties. We do not want to flat define 4 variations of each entity, but use some kind of inheritance instead. Is there a way to introduce a BasePet
bearing all properties (set to optional) and let the 4 operation-specific DTOs extend it with just overriding the required
list?
pros:
- Easier implementation for client & server as optionality is clarified for all properties.
- The "pain" is handled in the API specification.
cons:
- The API specification grows in complexity. It is not clear to me if the desired inhertance is possible and how it is specified.
- Scalability & maintainability of the API may suffer.
So my question on this topic: How can the inhertiance of the DTOs be specified for OpenAPI 3.0? Are there better alternatives? I am happy for all suggestions concerning these thoughts on an API for basic CRUD operations.