I recently finished Democracy’s Data, The Hidden Stories in the U.S. Census and How to Read Them, and the book was the closest thing to capturing how I see the Application Programming Interface, or simply the API. I regularly get frustrated with my role in the API universe because of the conflicting force’s between not just API producer and consumer, but also the API service and tooling providers, and the investors who do or do not support them. I am fascinated with this intersection, but also repeatedly horrified by what occurs at this intersection at scale online today.
I study APIs not because I think there is a single right way or wrong way to do APIs. I study them because they are shaping who we are at scale within each moment of our increasingly online lives. APIs are defining who we are, and our usage of web and mobile applications like Facebook, TikTok, Instagram, and others are allowing API producers to shape who we are, and while we have some control within these digital templates to still be human, increasingly so much of what shapes us is decided very far out of our control. Like in the United States Census in Democracy’s data, I see stories in the API cracks I see just beneath the surface of our online lives, and this is my attempt to tell a single story about our perpetual negotiation of where we are in the world, which used to determined by our physical home, as described in Democracy’s Data, but increasingly is a real-time mix of IP addresses bound to longitude and latitude.
Let’s Begin with Our Address
Let’s begin this journey with the negotiation of what is in the world which is within our control. Something we regularly negotiate online and offline—our address. Our address seems like a pretty straightforward thing to many of us, but if you’ve worked on a census, for any global business, or other complicated human scenarios, you know it is anything but straightforward. For me, the address represents so much about who we are in the world, which is something that has been evolved and abused significantly in an always online world.
A Machine-Readable Contract
In the world of APIs we have what is called the OpenAPI specification for describing the surface area of APIs. This YAML or JSON definition is used to describe an API for generating documentation and software development kits, but for me it is first and foremost about describing the surface area of the APIs that are governing our lives so that we can have a conversation about them. So, for this exercise I am going to use OpenAPI to describe and narrate the negotiation that occurs around where we are in the world. Instead of walking through each element of our Where We Are in the World API, I’ll start with this base definition.
# A machine and human readable contract.
openapi: 3.0.0
info:
# Title and description of what we are defining.
title: 'Where We Are In The World'
description: 'This is an OpenAPI contract for negotiating an address.'
# How we communicate changes.
version: 1.0.0
# Who is doing this work?
contact:
name: Kin Lane
url: https://apievangelist.com
email: [email protected]
# Legal
license:
name: Attribution 4.0 International (CC BY 4.0)
url: 'https://creativecommons.org/licenses/by/4.0/'
# Tags for organizing these thoughts and interfaces.
tags:
# A general descriptor of what is an address
- name: Address
description: 'The particulars of the place where someone lives or an organization is situated.'
# The ways in which this is accessed.
paths: {}
# These are centralized components referenced and reused across this definition.
components:
examples:
Address:
description: A basic example of an address.
value:
address: 123 Main Street.
# JSON Schema used to validate objects.
schemas:
Address:
type: object
description: 'Our address object.'
properties:
address:
type: string
description: 'This is the address.'
My goal here is to define an API that could be used to power a contact form on a website, or many contact forms across many websites. However, I do not have any paths, or even a base URL for my API. At this point I just want to use this as a contract for defining an address, providing a machine and human readable way to collaborate and iterate on what an address means — ensuring we are all on the same page. First, OpenAPI provides me with a way of defining the basics, like the name and description of my API, but also who to contact, and license being applied. I have added a single tag, but there are no paths, and I am mostly interested in fleshing out what an address is using the components object, using an example, but then also ensuring their is a JSON Schema for my example—-showing what an address is, while also articulating the shape of an address in a precise way.
Iterating Upon Our Address
Right after establishing the baseline for our address, it becomes clear that we will need more structure to our definition to make it support our needs. We need more than just the street, and we don’t want to just pack everything into an address. We need a city, state or province, a zip code, and country. As soon as I suggest a country, I immediately realize that zip code won’t work, and change my property to be postal code. To help articulate my changes I update the example and the supporting schema to reflect my intent—helping everyone else on my team get on the same page.
# A machine and human readable contract.
openapi: 3.0.0
info:
# Title and description of what we are defining.
title: 'Where We Are In The World'
description: 'This is an OpenAPI contract for negotiating an address.'
# How we communicate changes.
version: 1.1.0
# Who is doing this work?
contact:
name: Kin Lane
url: https://apievangelist.com
email: [email protected]
# Legal
license:
name: Attribution 4.0 International (CC BY 4.0)
url: 'https://creativecommons.org/licenses/by/4.0/'
# Tags for organizing these thoughts and interfaces.
tags:
- name: Address
description: 'The particulars of the place where someone lives or an organization is situated.'
- name: Countries
description: 'Expanding beyond just a single location, and placing each address within a country.'
# The ways in which this is accessed.
paths: {}
# These are centralized components referenced and reused across this definition.
components:
examples:
Address:
description: A basic example of an address.
value:
address: 123 Main Street.
city: New York
province: New York
postalCode: 10019
country: United States
# JSON Schema used to validate objects.
schemas:
Address:
type: object
description: 'Our address object.'
properties:
address:
type: string
description: 'This is the address.'
city:
type: string
description: 'This is the city.'
province:
type: string
description: 'This is the state or province.'
postalCode:
type: string
description: 'This is the postal code.'
country:
type: string
description: 'This is the country.'
I am pretending like I have a group of people negotiating this address API–taking a schema-driven approach to designing an API, and negotiating changes, expands the value of the API by adding constraints like country, which influences our zip and postal code property. Our examples and schema provide a precise way for us to articulate, iterate, and expand on our address API, in a collaborative way. This is an API contract. Everyone involved is getting to negotiate what an address is, from the team producing, to the developers consuming, and even the end-users of applications that will put the address API to use.
Constraining the Shape of Our Address
Before we agree on this definition of the object we will return using our address object, we want to not just define the name, but also the shape of our address properties. Our address, city, province, postalCode, and country are all strings, but let’s narrow this down further using the JSON Schema properties of our OpenAPI contract. These additional constraints can be used by the gateway we use to deploy our API, as well as the applications using the API to validate the input of addresses into our API.
# A machine and human readable contract.
openapi: 3.0.0
info:
# Title and description of what we are defining.
title: 'Where We Are In The World'
description: 'This is an OpenAPI contract for negotiating an address.'
# How we communicate changes.
version: 1.2.0
# Who is doing this work?
contact:
name: Kin Lane
url: https://apievangelist.com
email: [email protected]
# Legal
license:
name: Attribution 4.0 International (CC BY 4.0)
url: 'https://creativecommons.org/licenses/by/4.0/'
# Tags for organizing these thoughts and interfaces.
tags:
# A general descriptor of what is an address
- name: Address
description: 'The particulars of the place where someone lives or an organization is situated.'
# The ways in which this is accessed.
paths: {}
# These are centralized components referenced and reused across this definition.
components:
examples:
Address:
description: A basic example of an address.
value:
address: 123 Main Street.
city: New York
province: New York
postalCode: 10019
country: United States
# JSON Schema used to validate objects.
schemas:
Addresses:
type: array
items:
$ref: "#/components/schemas/Address"
Address:
type: object
description: 'Our address object.'
properties:
address:
type: string
description: 'This is the address.'
pattern: '^(\d{1,}) [a-zA-Z0-9\s]+(\,)? [a-zA-Z]+(\,)? [A-Z]{2} [0-9]{5,6}$'
minimum: 5
maximum: 100
city:
type: string
description: 'This is the city.'
pattern: '^[a-zA-Z.-]+(?:[\s-][\/a-zA-Z.]+)*$'
minimum: 5
maximum: 50
province:
type: string
description: 'This is the state or province.'
pattern: '/^[a-zA-Z0-9 ]*$/'
minimum: 5
maximum: 50
postalCode:
type: string
description: 'This is the postal code.'
pattern: '/^[a-zA-Z0-9 ]*$/'
minimum: 5
maximum: 25
country:
type: string
description: 'This is the country.'
pattern: '/^[a-zA-Z0-9 ]*$/'
minimum: 5
maximum: 50
Now we have a regular expression (regex), as well as a minimum and maximum length for each of our address properties. This gives more shape to our address object, constraining the properties in a way that can be validated by gateways, user interfaces, and anywhere else you can run AJV (Another JSON Schema Validator). Additionally, we can validate our entire OpenAPI contract for our address API using the JSON Schema for the OpenAPI specification. This schema-driven approach keeps our address negotiation more structured, precise, and machine-readable, ensuring that we keep addresses as clean as possible in our API.
Retrieving Our Addresses Using an API
Now we are ready to add an API path for accessing our addresses. This path can be appended to the HTTP URL for our API, allowing a JSON list of addresses to be returned. Our API uses HTTP to access our addresses, returning valid JSON according to our specification. Our single API operation references the JSON Schema iterated upon, returning an array of valid addresses, along with a 200 HTTP status code signaling a successful API call.
# A machine and human readable contract.
openapi: 3.0.0
info:
# Title and description of what we are defining.
title: 'Where We Are In The World'
description: 'This is an OpenAPI contract for negotiating an address.'
# How we communicate changes.
version: 1.3.0
# Who is doing this work?
contact:
name: Kin Lane
url: https://apievangelist.com
email: [email protected]
# Legal
license:
name: Attribution 4.0 International (CC BY 4.0)
url: 'https://creativecommons.org/licenses/by/4.0/'
# Tags for organizing these thoughts and interfaces.
tags:
# A general descriptor of what is an address
- name: Address
description: 'The particulars of the place where someone lives or an organization is situated.'
# The ways in which this is accessed.
paths:
'/addresses':
get:
operationId: getAddresses
summary: Addresses
description: 'Returns a list of addresses.'
tags:
- Address
responses:
"200":
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/Addresses"
example:
value:
address: 123 Main Street.
city: New York
province: New York
postalCode: 10019
country: United States
# These are centralized components referenced and reused across this definition.
components:
examples:
Address:
description: A basic example of an address.
value:
address: 123 Main Street.
city: New York
province: New York
postalCode: 10019
country: United States
# JSON Schema used to validate objects.
schemas:
Addresses:
type: array
items:
$ref: "#/components/schemas/Address"
Address:
type: object
description: 'Our address object.'
properties:
address:
type: string
description: 'This is the address.'
pattern: '^(\d{1,}) [a-zA-Z0-9\s]+(\,)? [a-zA-Z]+(\,)? [A-Z]{2} [0-9]{5,6}$'
minimum: 5
maximum: 100
city:
type: string
description: 'This is the city.'
pattern: '^[a-zA-Z.-]+(?:[\s-][\/a-zA-Z.]+)*$'
minimum: 5
maximum: 50
province:
type: string
description: 'This is the state or province.'
pattern: '/^[a-zA-Z0-9 ]*$/'
minimum: 5
maximum: 50
postalCode:
type: string
description: 'This is the postal code.'
pattern: '/^[a-zA-Z0-9 ]*$/'
minimum: 5
maximum: 25
country:
type: string
description: 'This is the country.'
pattern: '/^[a-zA-Z0-9 ]*$/'
minimum: 5
maximum: 50
Now we have an API. It is a simple example of the power of JSON Schema and OpenAPI. This API can now be mocked using a variety of commercial and open source tools, utilizing the example to emulate the behavior of our API. Since our API is defined as an OpenAPI you can easily render using common commercial and open source API documentation tooling. We have an API contract, documentation, and mocked implementation of our API, which we can use to further iterate on our API, and collaborate with our team, potential consumers, as well as end-users of the applications who put the API to work.
Create, Read, Update, and Delete (CRUD)
To put the finishing touches on our API, we want the ability to not just read addresses, but we also need to create, update, and delete addresses. HTTP provides us with a handful of HTTP methods for articulating a create with POST, updated with PUT, and delete with, well DELETE. This will expand our API to have two separate paths, with a total of five operations that round off our address API into something that is pretty valuable to application developers.
# A machine and human readable contract.
openapi: 3.0.0
info:
# Title and description of what we are defining.
title: 'Where We Are In The World'
description: 'This is an OpenAPI contract for negotiating an address.'
# How we communicate changes.
version: 1.4.0
# Who is doing this work?
contact:
name: Kin Lane
url: https://apievangelist.com
email: [email protected]
# Legal
license:
name: Attribution 4.0 International (CC BY 4.0)
url: 'https://creativecommons.org/licenses/by/4.0/'
# Tags for organizing these thoughts and interfaces.
tags:
# A general descriptor of what is an address
- name: Address
description: 'The particulars of the place where someone lives or an organization is situated.'
# The ways in which this is accessed.
paths:
'/addresses':
get:
operationId: getAddresses
summary: Addresses
description: 'Returns a list of addresses.'
tags:
- Address
responses:
"200":
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/Addresses"
example:
value:
- addressId: 1
address: 123 Main Street.
city: New York
province: New York
postalCode: 10019
country: United States
post:
summary: Add Address
description: 'Adds a new address.'
operationId: addAddress
tags:
- Address
requestBody:
$ref: "#/components/requestBodies/Address"
responses:
"201":
description: Successfully Added
content:
application/json:
schema:
$ref: "#/components/schemas/Address"
'/addresses/{addressId}':
get:
operationId: getAddress
summary: Retrieve Address
description: 'Returns a single address.'
parameters:
- name: addressId
description: 'The unique address id.'
in: path
required: true
schema:
type: string
tags:
- Address
responses:
"200":
description: Success
content:
application/json:
schema:
$ref: "#/components/schemas/Address"
example:
value:
addressId: 1
address: 123 Main Street.
city: New York
province: New York
postalCode: 10019
country: United States
put:
summary: Update Address
description: 'Updates an address.'
operationId: updateAddress
parameters:
- name: addressId
description: 'The unique address id.'
in: path
required: true
schema:
type: string
tags:
- Address
requestBody:
$ref: "#/components/requestBodies/Address"
responses:
"204":
description: Successfully Added
delete:
summary: Delete Address
description: 'Deletes an address.'
operationId: deleteAddress
parameters:
- name: addressId
description: 'The unique address id.'
in: path
required: true
schema:
type: string
tags:
- Address
responses:
"204":
description: Successfully Added
# These are centralized components referenced and reused across this definition.
components:
# The Request Body for Address
requestBodies:
Address:
content:
application/json:
schema:
$ref: "#/components/schemas/Address"
required: true
# Example JSON of Address
examples:
Address:
description: A basic example of an address.
value:
addressId: 1
address: 123 Main Street.
city: New York
province: New York
postalCode: 10019
country: United States
# JSON Schema used to validate objects.
schemas:
Addresses:
type: array
items:
$ref: "#/components/schemas/Address"
Address:
type: object
description: 'Our address object.'
properties:
address:
type: string
description: 'This is the address.'
pattern: '^(\d{1,}) [a-zA-Z0-9\s]+(\,)? [a-zA-Z]+(\,)? [A-Z]{2} [0-9]{5,6}$'
minimum: 5
maximum: 100
city:
type: string
description: 'This is the city.'
pattern: '^[a-zA-Z.-]+(?:[\s-][\/a-zA-Z.]+)*$'
minimum: 5
maximum: 50
province:
type: string
description: 'This is the state or province.'
pattern: '/^[a-zA-Z0-9 ]*$/'
minimum: 5
maximum: 50
postalCode:
type: string
description: 'This is the postal code.'
pattern: '/^[a-zA-Z0-9 ]*$/'
minimum: 5
maximum: 25
country:
type: string
description: 'This is the country.'
pattern: '/^[a-zA-Z0-9 ]*$/'
minimum: 5
maximum: 50
There are a number of other things we need to do to prepare our API for development, such as adding 400, 404, and 500 error responses for our APIs, but this provides us with a base contract for our API. We can develop and deploy our API, use our contract for documentation, but also as a baseline for ongoing negotiations with developers building applications, but also end-users of applications. This OpenAPI provides a precise way to articulate what creating, reading, updating, and deleting addresses mean, but also adding new properties such as address2, longitude, latitude, and introducing new paths and operations for handling directions and other address enabled capabilities.
A Record of Negotiations
The [Semantic Versioning[(https://semver.org/)] that OpenAPI provides, allows us to articulate which version of OpenAPI we are using, which also defines which version of JSON Schema we are using, as well as communicate the change of our address schema and API. I am exploring other ways of documenting the actual negotiations of our API contract, providing the reason behind the addition of paths, operations, schema, and properties. I am also thinking about how OpenAPI can record suggestions for changes by API designers, architects, or even developers and end-users. I am looking to add a change management extension layer to OpenAPI, or possibly some sort of change management overlay using an APIs.json.
I am getting better at documenting my own APIs using this approach. While I am not technically documenting the negotiation of anything when an API is designed, developed, and deployed by a team of one. I am documenting change and recording the provenance of why I made certain decisions, which helps me out down the road when I go to iterate on an API I haven’t touched in a while. Everything I need is right there. Plus there is a way to hang issues and feedback from consumers and end-users, so I can rapidly gather what is needed to iterate on my API in more meaningful ways. All of this is why I use OpenAPI and JSON Schema. Sure, the documentation and mock servers are a nice benefit of a schema-driven approach, but it really is the structure and constraints of delivering APIs using a contract. It just helps me stay on my game, and deliver APIs that are consistent, robust, and useful to consumers.