DNSimple API v2 OpenAPI definition in depth
In my previous post I announced our adoption of OpenAPI v3 to describe the DNSimple API. I went into why we choose to adopt OpenAPI v3, the tools we used, and some of the challenges we faced. In this post I am diving into the details of our OpenAPI definition for the DNSimple API, showing how OpenAPI elements describe API functionality.
One quick note: throughout this post, whenever I write OpenAPI, I mean OpenAPI v3, the version of the OpenAPI specification that we are using.
Getting Started
The definition for our API is available in both JSON and YAML versions. We edit the YAML version of the document due to its simple and barebones syntax and the ease of editing in Swagger, and then we generate the JSON version.
Open the YAML version either in your web browser or in an editor. For this post, I will use the Swagger editor.
The basic structure of an OpenAPI definition defines the semantic version of OpenAPI first in an element openapi
, followed by an info
element that contains metadata about the API that is being described. The final required element is the paths
element, which contains all of the available paths and operations for the API.
In addition to the required elements, there are a number of other top-level elements that can provide additional details about your API or help provide reusable elements for the API definition. I'll go over those a bit more later in the post. For now, here is what the top level of our definition looks like:
openapi: 3.0.0
info: ...
externalDocs: ...
security: ...
servers: ...
paths: ...
components: ...
Info
The required info element contains the title and version of our API, contact details for where you can get assistance for the API, our terms of service, and a textual description of the API.
info:
title: DNSimple API
version: 2.0.0
contact:
name: DNSimple Support
email: support@dnsimple.com
url: 'https://dnsimple.com/contact'
excerpt: >-
[DNSimple](https://dnsimple.com) provides DNS hosting and domain
registration that is simple and friendly. We provide an extensive API and an
easy-to-use web interface so you can get your domain registered and set up
with a minimal amount of effort.
termsOfService: 'https://dnsimple.com/terms'
The info section is pretty straightforward. Only the title and version are required, but including other fields may be helpful to developers working with your API.
Servers
The servers element lists servers that developers working with your API can connect to.
servers:
- url: 'https://api.dnsimple.com/v2'
excerpt: DNSimple Production API
variables: {}
- url: 'https://api.sandbox.dnsimple.com/v2'
excerpt: >-
DNSimple Sandbox API. For more information on the purpose and use of the Sandbox API, see our [testing documentation](https://developer.dnsimple.com/v2/#testing)
We include both our production API as well as our sandbox API. Since the description element accepts markdown, we have the ability to link to other documentation, as is shown in the sandbox example, which explains how to properly test using the sandbox environment.
You can also use variables in your server URL and define those here, but we currently do not take advantage of this feature.
Security
The security element describes what kind of authentication methods are available to access the API. We provide two types of authentication in API v2: basic authentication with a username and password and token authentication with a bearer token.
security:
- basicAuth: []
- bearerAuth: []
Even though I have not gone through the components
element yet (I will below) it's important to show how each of the named security methods are defined in components. Here is the relevant bit of the components
element:
securitySchemes:
basicAuth:
type: http
scheme: basic
bearerAuth:
type: http
scheme: bearer
As you can see, each security item has a corresponding security scheme. Both of the scheme types are http, with one using the basic scheme and other the bearer scheme. You can find more on the various security scheme types in the OpenAPI spec.
One benefit of specifying your security methods is that the swagger generated documentation will actually allow consumers to connect to their account and experiment with the API through the documentation.
Paths and Components
Now that I've covered the elements of the definition that are high-level and that apply to the API, it's time to dig into the paths
element (and along with that the components
element). Paths hold the relative paths to the individual endpoints and their operations. This is where the meat of your definition is and where you will spend quite a bit of time if your API is large.
Let's take a look at the first API path almost anyone using the DNSimple API will touch: the whoami
path. The result of a call to this API path returns a JSON document with the User and/or Account details for your account based on how you authenticated.
/whoami:
get:
excerpt: >-
Get details about the current authenticated entity used to access the API.
parameters: []
operationId: whoami
tags:
- identity
responses:
'200':
excerpt: Successful response with user or account.
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
account:
$ref: '#/components/schemas/Account'
user:
$ref: '#/components/schemas/User'
'401':
$ref: '#/components/responses/401'
'429':
$ref: '#/components/responses/429'
default:
$ref: '#/components/responses/Error'
The first line specifies the relative path. Thus the full path is https://api.dnsimple.com/v2/whoami in the case of our production endpoint. Under the path you have the HTTP verbs that are supported for that path. In this case, only GET
is supported. Note that get
is lowercase. The get
element holds details about the operation that is executed when that path is called with the given HTTP method. The operation can contain a bunch of details. We've opted to have a description
for each operation, the collection of parameters
(which may be empty, as in this case), an operationId
which is a unique string used to identify the operation. We use the same string throughout our internal documentation. We also include a tags list to help group our API paths together in logical groups (such as identity
or domains
).
The only required element in an operation object is the responses
element. This is a map of response codes to their expected response. In this example, the response code is 200
which indicates a "successful response with user or account." This response code is mapped to a response object that contains a description, headers, content, and links. In the case of our API, we have only specified a description and the content at the moment. Content maps mime types to media type objects, which can specify the schema (what we use) as well as examples and encoding.
At this point, it's probably best to dive into an example and see how this maps back to the structure of the OpenAPI definition.
Example
If I authenticate with a bearer Account token then I would receive something like the following:
{
"data":{
"user":null,
"account":{
"id":1,
"email":"john.doe@example.com",
"plan_identifier":"dnsimple-personal",
"created_at":"2018-02-12T15:41:21Z",
"updated_at":"2018-02-15T22:14:15Z"
}
}
}
Let's look again at the successful response object's definition:
'200':
excerpt: Successful response with user or account.
content:
application/json:
schema:
type: object
properties:
data:
type: object
properties:
account:
$ref: '#/components/schemas/Account'
user:
$ref: '#/components/schemas/User'
Given a 200 response from a call to /whoami
using a GET verb, the content type is application/json
, and the response is a JSON object. It has the properties data
which in turn references either an account
object and/or a user
object. The User and Account object types are references to their definitions in the components
element. By defining definitions of the various objects that are returned from our service, we can greatly reduce the amount of work for maintaining the definition and reduce the likelihood of making silly mistakes.
We also have documented many of the possible error responses.
'401':
$ref: '#/components/responses/401'
'429':
$ref: '#/components/responses/429'
default:
$ref: '#/components/responses/Error'
Again, components help us by letting us only define each error definition once, in the components section.
components:
securitySchemas: ...
links: {}
callbacks: ...
parameters: ...
schemas: ...
responses: ...
requestBodies: ...
headers: ...
I've already gone through what's in securitySchemas
, so I won't cover that again. Our links
object is currently empty, however we may augment our definition by including this in the future. Let's have a look at the schemas, as we referenced these in the example above.
Schemas
Here is the Account
schema:
Account:
type: object
properties:
id:
type: integer
email:
type: string
plan_identifier:
type: string
created_at:
$ref: '#/components/schemas/DatetimeISO8601'
updated_at:
$ref: '#/components/schemas/DatetimeISO8601'
example:
id: 1
email: example-account@example.com
plan_identifier: dnsimple-professional
created_at: '2015-09-18T23:04:37Z'
updated_at: '2016-06-09T20:03:39Z'
It specifies the type of the Account
, which is an object, as well as its properties and an example. The properties is a map of property names to their types or references to other schemas. The created_at
and updated_at
properties refer to a schema called DatetimeISO8601 #/components/schemas/DatetimeISO8601
. This type is currently defined as a string
several lines before the Account
type, but in the future we could change it to be something that better represents dates if the API is updated to reflect the new type. This is one of the benefits of using custom-defined types rather than just saying everything is a primitive type. It also makes it clear what it is semantically (an ISO 8601 date, not just a string). The example element provides example data. Note that the example overrides the schema definitions, so make sure that its keys match the keys in the schema.
Another interesting schema is the WebhookPayload
schema. One of the challenges with webhooks is enumerating all of the different webhooks that may be sent. OpenAPI v3 makes it easy to do this by providing the enum
attribute. The WebhookPayload name
property uses the enum
element to list all of the possible webhook names that may be sent. The data
element uses the oneOf
selector to indicate that the data schema in the webhook will be one of the various defined types. Again, by being able to enumerate all of the possible types it makes it much easier to show consumers what data may arrive.
Parameters, Request Bodies, and Responses
In addition to the schema element, we also use the parameters, request bodies, and responses elements in the components section. Defining these components in one location simplifies reuse and ensures consistency throughout our definition document. Parameters are used to define what data is passed when calling the API. Request bodies define the HTTP bodies that are expected for certain API calls. Finally, responses defines the structure of the response objects.
Conclusion
If you look through the OpenAPI 3 specification, and consider the complexity of our definition, you can see that there is a lot that can be defined. Your needs should dictate how much or how little of the OpenAPI specification you are interested in using. Regardless of how much you choose to implement, OpenAPI provides an excellent, comprehensive framework for defining your APIs.
Anthony Eden
I break things so Simone continues to have plenty to do. I occasionally have useful ideas, like building a domain and DNS provider that doesn't suck.
We think domain management should be easy.
That's why we continue building DNSimple.
4.3 out of 5 stars.
Based on Trustpilot.com and G2.com reviews.