structuring an OpenAPI description
8. October 2023
An OpenAPI description can be used to generate code that helps to implement the server side part of the api. This has a couple of advantages.
For me the biggest advantages of generating annotation based controller interfaces are:
-
I don’t have to remember all the details and parameters of the controller annotations
-
the annotation "noise" is hidden in an interface
-
it is easier to create a more consistent api because it is NOT spread over a number of source files
-
the api is an explicit artifact and not implicitly derived from the implementation
writing an OpenAPI description
Of course, writing an OpenAPI description has its own challenges. It is not very difficult (with IDE support), but you need to structure it in a manageable way.
Having everything in a single file is not a good idea if it starts to grow. It doesn’t work for source code, and it doesn’t work very for an OpenAPI description.
Here is one possible way to structure an OpenAPI document into multiple files and directories to keep track of the document and to make it easier to navigate.
The solution is to split up the OpenAPI document by using $ref
s at a couple of places and a simple naming schema for the extracted files.
It helps to find the description of an endpoint when we have the endpoint path and are looking for its file or the other way round, finding out what the endpoint path is when we have the file.
path item $refs
The first place where we use $ref
s is at the path items. It allows us to move the full description of an endpoint to a separate file.
openapi: 3.1.0
info:
title: sample API
version: 1.0.0
paths:
/foo:
$ref: 'foo.yaml'
/bar:
$ref: 'bar.yaml'
/foo2:
$ref: 'foo/foo.yaml'
An OpenAPI parser will find foo.yaml
and bar.yaml
if they are siblings of the OpenAPI root file. We can also place them in subdirectories like in the last example.
path head to directory
Usually I create a folder for each group of endpoints. Endpoints form a group if they start with the same path element (the path head). Let’s say we have the following endpoints around the topic foo:
paths:
/foo:
# ...
/foo/parts/{part}:
# ...
/foo/parts/fixed:
# ...
/foo/parts/{part}/bars/{bar}/fix-ed:
# ...
They all start with /foo
and build an endpoint group that goes into a subdirectory foo
:
|--- openapi.yaml
\--- foo
|--- ...
\--- ...
When we are looking for an api that starts with /foo
we know we will find it in the subdirectory foo
.
It is possible to create more directory levels for more common path elements, but we don’t want a deeply nested tree that makes it harder to navigate.
I use only a single directory level with a simple naming schema for the endpoints of the group.
path tail to file name
The naming schema is to separate all parts of the path tail by underscores and replace path parameters with an x
character.
Using the naming schema for the above endpoints we will get the following directory layout:
|--- openapi.yaml
\--- foo
|--- _.yaml (1)
|--- parts_x.yaml (2)
|--- parts_fixed.yaml (3)
\--- parts_x_bars_x_fix-ed.yaml (4)
1 | _.yaml is the file for the /foo endpoint. The /foo endpoint has a head (the directory) but no tail. Its empty tail is represented by the underscore. |
2 | parts_x.yaml is the file for the endpoint /foo/parts/{part} . Its head is /foo (i.e. the directory) and its tail is /parts/{part} with the parts /parts and the parameter {part} .
Applying the naming schema we get |
3 | parts_fixed.yaml is the file for the endpoint /foo/parts/fixed . Again its head is /foo (i.e. the directory) and its tail is parts/fixed . No path parameters.
Applying the naming schema we get |
4 | this follows the same simple naming schema. :-) |
path item file content
A path item file contains the extracted endpoint with all used HTTP methods.
This is the second place where we can extract part of it with more $ref
s to make it easier to read. Extracting makes sense for parameters or schemas, especially if they are used in multiple endpoints.
This could look like this:
get:
summary: an example endpoint description
operationId: get_foo
parameters:
- $ref: 'resources.yaml#/parameters/path/APathParameter'
- $ref: 'resources.yaml#/parameters/query/AQueryParameter'
- $ref: '../common/resources.yaml#/parameters/path/CommonPathParameter'
responses:
200:
description: an example response schema
content:
application/json:
schema:
type: array
items:
$ref: "resources.yaml#/schemas/Foo"
put:
# ....
The parameters and the response schema are extracted to a file named resources.yaml
in the same directory. It collects all extracted items used by the endpoint group (see next section).
If we have some common parameters or schemas used by multiple otherwise unrelated endpoint groups (i.e. from different folders) we can put them in its own directory (here ../common
).
|--- openapi.yaml
|--- foo
| |--- _.yaml
| |--- parts_x.yaml
| |--- parts_fixed.yaml
| |--- parts_x_bars_x_fix-ed.yaml
| \--- resources.yaml
\--- common
\--- resources.yaml
resources.yaml
The resources.yaml
could be:
schemas:
Foo:
# ...
parameters:
path:
APathParameter:
- name: pathParamName
in: path
# ...
query:
- name: queryParamName
in: query
# ...
The naming and/or nesting are created as needed.
For example, if an endpoint group just uses one type of parameter we don’t need the path
/query
separation. We can separate path or query parameters by another attribute or separate request and response schemas.
We can put all of this in multiple resource files. We can structure in any way that makes sense to us and our teammates.
summary
This article describes a simple way to structure handwritten OpenAPI files by grouping endpoints by their path into subdirectories using a simple naming schema.
It helps to find the description of an endpoint when we have the endpoint path and are looking for its file or the other way round, finding out what the endpoint path is when we have the file.
Use it as inspiration :-)