Recently I've taken the time to update the API documentation for bookmarks.dev-api. I used Swagger 2.0 (aka OAS 2) before and I decided to update to OpenAPI Specification (OAS) 3. In this post I will list the main points about the process and documenting the API with OAS 3. Some points might still apply to the former OAS 2 (fka Swagger) documentation, but they are worth mentioning since I hadn't payed enough attention before to them.
The code snippets examples presented in this article are from the OAS 3 specification for bookmarks.dev-api, which is available on Github at openapi.yaml. The result is available at bookmarks.dev/api/docs/
Here are the ten points to consider:
1. Read the A Guide to What’s New in OpenAPI 3.0 article
In this article, they share some of the major updates in the latest version of OAS, and breakdown what you need to know when transitioning to OAS 3.0. It's based on this webinar OpenAPI 3.0, And What it Means for the Future of Swagger - 1 hour long
2. Use the OpenAPI/Swagger 2.0 to OpenAPI 3.0 Converter WebService
to convert your Swagger specification to OpenAPI 3.0.
It is available online at https://converter.swagger.io/ and as a docker image:
docker pull swaggerapi/swagger-converter:v1.0.2
docker run -it -p 8080:8080 --name swagger-converter swaggerapi/swagger-converter:v1.0.2
3. Use Swagger-Editor to immediate validate your specification and preview it in real time
Swagger Editor lets you edit Swagger API specifications in YAML inside your browser and to preview documentations in real time.
You can use it online, as an npm distribution or as a docker image.
For more details check the Readme of the project.
4. Use Swagger UI to present your documentation
Swagger UI is a collection of HTML, Javascript, and CSS assets that dynamically generate beautiful documentation from a Swagger-compliant API.
I use it indirectly with the help of Swagger UI Express. That way you can access the Swagger UI documentation as a route in the API for example, in my case at bookmarks.dev/api/docs/
The code snippet from app.js
:
const swaggerUi = require('swagger-ui-express');
const YAML = require('yamljs');
const swaggerDocument = YAML.load('./docs/openapi/openapi.yaml');
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
It's helpful to include the open specification file (here
openapi.yaml
) in the nodemon watch (e.g.nodemon --inspect ./bin/www --watch src --watch docs/openapi/openapi.yaml
), so that you can reload the UI without having to manually restart the ExpressJS server
4.1 Use swagger-jsdoc for a code-first approach
Another point worth mentioning here is that you could use swagger-jsdoc,
to integrate Swagger using JSDoc
comments in your code. The swagger-jsdoc
project assumes that you want document your existing/living/working code in a way to "give life" to it, generating a specification which can then be fed into other Swagger tools, and not the vice-versa.
For now I manage documentation centrally in one openapi.yaml file, but I might consider it at a later time
5. Use tags to group the operations
You can assign a list of tags to each API operation. Thus Swagger UI and Swagger Editor will display the operations by
tags which comes very handy. To control the sorting in Swagger UI you need to add them also at the root level as global
tags. There you can also add a description and link to external documentation for them.
Here are the tags I use for the API for
tags:
- name: root
description: Used to mark the root endpoint
- name: version
description: Access to the project's version and gitSha1
- name: public-bookmarks
description: Access to public bookmarks
- name: personal-bookmarks
description: Operations performed on personal bookmarks
- name: user-data
description: Operations performed on user data
- name: helper
description: Helper endpoints/operations
6. Use the servers array to specify one or more base URLs for your API
In OpenAPI 3.0, you use the servers
array to specify one or more base URLs for your API. servers replaces the host
, basePath
and schemes
keywords used in OpenAPI 2.0. Each server has an url
and an optional Markdown-formatted description
.
servers:
- url: http://localhost:3000/api
description: Local server for development
- url: https://www.bookmarks.dev/api
description: Main (production) server
7. Use components in order to define and reuse resources
Often, multiple API operations have some common parameters or return the same response structure. To avoid code duplication, you can place the common definitions in the global components
section and reference them using $ref
.
For example for a list of bookmarks response that occurs with several operations I defined a BookmarkListResponse
under the global responses
section
components:
responses:
BookmarkListResponse:
description: List of bookmarks
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Bookmark'
and I reference it in the different operations (e.g. get-public-bookmarks
):
/public/bookmarks:
get:
summary: Returns list of public bookmarks filtered with query parameters.
tags:
- public-bookmarks
description: |
* if `q` param is present they are filtered by the query text contained in it. (takes precedence over `location`)
* if `location` param is present returns a list with one **public** bookmark with that URL
* else **defaults** to the latest added 100 public bookmarks
> The list is empty if not public bookmarks returned for filter
parameters:
- $ref: "#/components/parameters/searchTextQueryParam"
- $ref: "#/components/parameters/limitQueryParam"
- $ref: "#/components/parameters/locationQueryParam"
responses:
200:
description: OK
$ref: '#/components/responses/BookmarkListResponse'
Notice above also the locationQueryParam
. It's a location
query parameter defined in the components > parameters
section and then referenced in multiple places in the API specification (one of them above):
componentes:
parameters:
locationQueryParam:
name: location
in: query
description: location of the bookmark, usually an URL
required: false
schema:
type: string
8. Add examples to make it clearer
You can add examples to parameters, properties and objects to make OpenAPI specification of your web service clearer. Examples can be read by tools and libraries that process your API in some way. For example, an API mocking tool can use sample values to generate mock requests. You can specify examples for objects, individual properties and operation parameters. To specify an example, you use the example
or examples
keys.
For example the search text used to filter bookmarks can have complex values, and what better ways to explain it than with some examples:
components:
parameters:
searchTextQueryParam:
name: q
in: query
description: |
search query (terms are separated by space). There are special filters available:
* `lang:iso_language_code` - e.g. `lang:en` for English, `lang:es` for Spanish and `lang:de` for German bookmarks
* `site:site_URL` - e.g. `site:codepedia.org` bookmarks only from website [www.codepedia.org](https://www.codepedia.org)
* `userId:UUID-user` - to be used only when querying **public bookmarks** submitted by the user with `userId`
* `private:only` - makes sense **only** when used for querying **personal bookmarks**
schema:
type: string
examples: # Multiple examples
german:
value: 'lang:de'
summary: Will look only for bookmarks in German
site:
value: 'site:codepedia.org'
summary: Wille look only for bookmarks with the domain **codepedia.org**
complex:
value: 'exception handling [java] site:codepedia.org'
summary: Wille look only for bookmarks with terms "exception" and "handling" tagged with "java" and the domain **codepedia.org**
complex-private-only:
value: 'exception handling [java] site:wiki.my-corporation.com private:only'
summary: Same as above but only within **private** bookmarks
or show what a bookmark input for creation might look like, for different scenarios (normal article, youtube video or StackOverflow question):
paths:
/personal/users/{userId}/bookmarks:
post:
description: Create new bookmark for user
operationId: create-bookmark
tags:
- personal-bookmarks
parameters:
- $ref: '#/components/parameters/userIdPathParam'
requestBody:
description: Bookmark json data
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Bookmark'
examples:
blog-article:
value:
name: Cleaner code in an Express REST API with unified error handling – CodepediaOrg
location: https://www.codepedia.org/ama/cleaner-code-in-expressjs-rest-api-with-custom-error-handling
language: en
tags: [nodejs, error-handling, expressjs]
publishedOn: 2019-12-02,
sourceCodeURL: https://github.com/CodepediaOrg/bookmarks.dev-api
description: Shows how you can make your backend ExpressJS REST API cleaner by using custom error handling middleware. Code snippets of before and after refactoring are presented to make the point
descriptionHtml: <p>Shows how you can make your backend ExpressJS REST API cleaner by using custom error handling middleware. Code snippets of before and after refactoring are presented to make the point</p>
userId: 4c617f2b-2bad-498b-a9c6-4e9a8c303798
public: true,
lastAccessedAt: 2020-02-15T20:42:19.297Z
likeCount: 0
stackoverflow-question:
value:
name: Create GUID / UUID in JavaScript?
location: https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
language: en
tags: [javascript, guid, uuid]
publishedOn: 2008-09-19
sourceCodeURL:
description: UUIDs (Universally Unique IDentifier), also known as GUIDs (Globally Unique IDentifier), according to [RFC 4122](https://www.ietf.org/rfc/rfc4122.txt), are identifiers with a certain uniqueness guarantee.\n\nThe best way to generate them, is to follow implementation instructions in the said RFC, use one of the many community vetted open source implementations.\n\nA popular Open Source tool for working with UUIDs in JavaScript is [node-uuid](https://github.com/kelektiv/node-uuid)\n\nNote that just randomly generating the identifiers byte by byte, or character by character, will not give you the same guarantees as a conforming implementation. Also, very important, systems working with compliant UUIDs may choose not to accept randomly generated ones, and many open source validators will actually check for a valid structure.\n\nAn UUID must have this format:\n```
{% endraw %}
\nxxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx\n
{% raw %}
```\nWhere the M and N positions may only have certain values. At this time, the only valid values for M are 1, 2, 3, 4 and 5, so randomly generating that position would make most results unacceptable.
descriptionHtml: <p>UUIDs (Universally Unique IDentifier), also known as GUIDs (Globally Unique IDentifier), according to <a href=\"https://www.ietf.org/rfc/rfc4122.txt\">RFC 4122</a>, are identifiers with a certain uniqueness guarantee.</p>\n<p>The best way to generate them, is to follow implementation instructions in the said RFC, use one of the many community vetted open source implementations.</p>\n<p>A popular Open Source tool for working with UUIDs in JavaScript is <a href=\"https://github.com/kelektiv/node-uuid\">node-uuid</a></p>\n<p>Note that just randomly generating the identifiers byte by byte, or character by character, will not give you the same guarantees as a conforming implementation. Also, very important, systems working with compliant UUIDs may choose not to accept randomly generated ones, and many open source validators will actually check for a valid structure.</p>\n<p>An UUID must have this format:</p>\n<pre><code>xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx\n</code></pre>\n<p>Where the M and N positions may only have certain values. At this time, the only valid values for M are 1, 2, 3, 4 and 5, so randomly generating that position would make most results unacceptable.</p>
userId: 4c617f2b-2bad-498b-a9c6-4e9a8c303798
public: true
lastAccessedAt: 2020-02-15T20:59:45.447Z
likeCount: 0
stackoverflowQuestionId: 105034
youtube-video:
value:
name: Jake Archibald In The Loop - JSConf.Asia - 35min
location: https://www.youtube.com/watch?v=cCOL7MC4Pl0
language: en
tags: [javascript, event-loop, browser]
publishedOn: 2018-02-09
sourceCodeURL:
description: Have you ever had a bug where things were happening in the wrong order, or particular style changes were being ignored? Ever fixed that bug by wrapping a section of code in a setTimeout? Ever found that fix to be unreliable, and played around with the timeout number until it kinda almost always worked? \nThis talk looks at the browser's event loop, the thing that orchestrates the main thread of the browser, which includes JavaScript, events, and rendering. We'll look at the difference between tasks, microtasks, requestAnimationFrame, requestIdleCallback, and where events land. \nHopefully you'll never have to use setTimeout hacks again!\"
descriptionHtml: <p>Have you ever had a bug where things were happening in the wrong order, or particular style changes were being ignored? Ever fixed that bug by wrapping a section of code in a setTimeout? Ever found that fix to be unreliable, and played around with the timeout number until it kinda almost always worked? \nThis talk looks at the browser's event loop, the thing that orchestrates the main thread of the browser, which includes JavaScript, events, and rendering. We'll look at the difference between tasks, microtasks, requestAnimationFrame, requestIdleCallback, and where events land. \nHopefully you'll never have to use setTimeout hacks again!\"</p>
userId: 4c617f2b-2bad-498b-a9c6-4e9a8c303798
public: true
lastAccessedAt: 2020-02-15T21:12:12.670Z
likeCount: 0
youtubeVideoId: cCOL7MC4Pl0
9. Use enums to define constants
Open API does not support the const
keyword of JSON Schema yet
but you can use enum
to define one value. For example I know that the validation errors have a HTTP Status of 400 (Bad request) so I can model this the following way:
components:
schemas:
ValidationErrorModel:
allOf:
- $ref: '#/components/schemas/BasicErrorModel'
- type: object
required:
- validationErrors
- httpStatus
properties:
httpStatus:
type: integer
enum: [400]
validationErrors:
type: array
items:
type: string
Of course you can use enums to specify values for a request parameter or a model property, as they were intended. For example see the orderBy
possible values when filtering personal bookmarks
paths:
/personal/users/{userId}/bookmarks:
get:
parameters:
- $ref: "#/components/parameters/userIdPathParam"
- $ref: "#/components/parameters/searchTextQueryParam"
- $ref: "#/components/parameters/limitQueryParam"
- $ref: "#/components/parameters/locationQueryParam"
- name: orderBy
in: query
description: |
It is considered in the abscence of `q` or `location` parameters
Possible values:
* MOST_LIKES - personal bookmarks most liked by the community
* LAST_CREATED - personal bookmarks last added
* MOST_USED - personal bookmarks the user (owner) clicked the most
schema:
type: string
enum: [MOST_LIKES, LAST_CREATED, MOST_USED]
10. Bookmark the resources you might recall later
I have bookmarked quite a few Swagger/OpenAPI resources and tools along the way and made them public at my [openapi] public resources on bookmarks.dev
Conclusion
These were the ten points to remember when starting with OpenAPI Specification 3. Do you have others worth mentioning? Please leave a comment below with it if you do.
Top comments (0)