Developers like to consider themselves as creators—we create code, we create ideas, we create experiences. In the realm of APIs, we create interfaces so others can use our software. An important aspect of most APIs is their ability to create things as well. In the past1, we started at the end of the “CRUDL” initialism and discussed basic List operations in APIs. Next, we jump to the beginning of that “CRUDL” initialism and talk about the Create operations. Along the way, we’ll apply many of the skills we’ve covered in the previous dozen or so articles of the Language of API Design.
Welcome to the next article in the Language of API Design. Rather than jumping into the middle of this series, I encourage new subscribers/visitors start by reading Your Guide to The Language of API Design and scanning previous posts in the series.
Let’s get creative! Consider yourself a user—an author or content creator—of the Chain Links app. You want to create a new story. What’s more, you are really ambitious and you want to create an entirely new universe for that story. Good for you! Taking the role of creator to its limit!
To enable your creative juices, the Chain Link API needs an API operation to create a new Universe.
From our previous work, we already saw the skeleton for the API operations of Chain Link universes:
paths:
/universes:
get:
operationId: listUniverses
summary: List the universes
description:
Return a paginated list of the universes in the system.
/universes/{universeId}:
...
Within this structure which defines the paths
for the universes collection (/universes
) and universe instances (/universes/{universeId}
), we can fit the basic Create, Read, Update, Delete, List (CRUDL) operations. This is done by following a common API design pattern:
Create
POST /universes
operationId:
createUniverse
Read
GET /universes/{universeId}
operationId:
getUniverse
Update
PUT /universes/{universeId}
operationId:
updateUniverse
PATCH /universes/{universeId}
operationId:
patchUniverse
Delete
DELETE /universes/{universeId}
Careful—that’s a lot of power!
operationId:
deleteUniverse
List
GET /universes
operationId:
listUniverses
To create a universe, we need to pass some data. This is expressed as a request body object in an OpenAPI document. The request body consists of a media type (application/json
in our case) and a schema
to define the form of the data. A request body can also accept request headers, but we do not need them for this operation. OpenAPI allows an operation to define multiple media types, each with independent schemas to describe their data. Thus, the Create Universe operation can consume application/json
as well as form data, if you like.
A universe needs a few things to bring it to life:
a name
a description
a creator (that’s you)
an optional (existing) base universe that this one is based on. (The new universe may be a variation of the other… maybe even one universe in a multiverse.)
Here’s some simple JSON data with only the essential data that will do the trick:
{
"name": "DragonTerr",
"description":
"A world where dragons rule, but not without challengers."
}
To keep things secure2, our API requires the author to be logged in. Therefore, the API operation can infer the universe’s creator from the user’s authorization. This also means the API does not have to worry about validating input data related to the universe’s creator, hence there is one less problem to worry about in the implementation. This may seem like a trivial matter, but it is a useful application of the design principal of convention over configuration. This leads to APIs that are more concise, less verbose, not too wordy, more succinct, more compact, and having a smaller footprint.
We need a JSON schema for the request body. The convention I use when creating a new resource is to name the schema with a “new” prefix, followed by the name of the resource. Thus, we will use the schema name newUniverse
to create a new universe. Both the name
and description
properties are required for a new universe. Here is the initial stub for the Create Universe operation, using the operationId
of createUniverse
, and specifying the request body. Note: unless we explicitly declare required: true
for the requestBody
, the body will be optional and the API will allow a client to call the operation with no request body. So be sure to add this constraint is your operation always requires a request body.
paths:
/universes:
post:
operationId: createUniverse
summary: Create a new universe.
tags:
- Universes
description: Create a new universe in the collection of universes.
requestBody:
description: A new universe to create.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/newUniverse'
components:
schemas:
newUniverse:
title: Request body to create a new universe.
description: >-
Properties to create a new universe in which authors can
create new characters and new chains.
type: object
required:
- name
- description
unevaluatedProperties: false
properties:
name:
description: The name of the universe.
minLength: 4
maxLength: 64
description:
description: The description of the universe.
minLength: 4
maxLength: 512
sourceUniverse_url:
description: >-
An optional universe that this one is derived from.
type: string
format: uri_reference
example:
name: DragonTerr
description: >-
A world where dragons rule, but not without challengers.
(The operation should also declare the security requirement of the operation, but we don’t have room to go into that detail here, so please look for a future article on API security. I promise I’ll get to it!)
We also need to decide what the Create Universe operation should return. (see What Am I Getting Out of This? for more details). The HTTP 201 Created response code should be used when an API operation directly creates a new resource. However, the next question is, “What response body should the Create Universe operation return?” Different APIs address this in different ways. As I’ve suggested earlier, decide how you want to answer such design questions, then do so consistently across the entirety of your API.
The response should include the URL of the new instance, or at a minimum, the new resource’s unique ID (the {universeId}
in this case). I find it convenient for the Create Universe response to be a full JSON object representation, identical to what the Read Universe operation returns. If our API did not return this, most clients will turn around and call the Read Universe operation anyway, so this saves an extra API call for most clients. (The Read Universe operation result will include other derived properties of the object besides the resource’s id
and url
, such as the creation timestamp, links to related resources, etc.). It is also convenient to include the Location response header with the HTTP 201 Created response code.
The response schema needs a name, so let’s call it universe
. This is a style issue. Some APIs use a naming pattern of {resource}Request
for a request body and {resource}Response
for a response body. I find those suffixes unnecessary and verbose and prefer names that are more concise, less verbose, not too wordy, more succinct, more compact, and having a smaller footprint.
Here is the universe
schema definition:
components:
schemas:
universe:
title: Universe
description: >-
A universe in which authors can
create new characters and new chains.
type: object
required:
- id
- name
- description
- createdAt
- creator_url
- characters_url
- chains_url
unevaluatedProperties: false
properties:
id:
$ref: '#/components/schemas/resourceId'
name:
description: The name of the universe.
minLength: 4
maxLength: 64
description:
description: The description of the universe.
minLength: 4
maxLength: 512
sourceUniverse_url:
description: >-
An optional universe that this one is derived from.
type: string
format: uri_reference
createdAt:
description: >-
The date and time the author created universe,
in RFC 3339 date-time format.
type: string
format: date-time
creator_url:
description: The URL of this universe's creator/author.
type: string
format: uri-reference
characters_url:
description: >-
The URL of the API operation to list
the characters that exist in this universe.
type: string
format: uri-reference
chains_url:
description: >-
The URL of the API operation to list
the chains that exist in this universe.
type: string
format: uri-reference
example:
id: uni-489f34dhj37sghj
name: DragonTerr
description: >-
A world where dragons rule, but not without challengers.
createdAt: '2023-08-23T18:34:10.444Z'
creator_url: /authors/au-ndklxhjf8933x0
characters_url: /characters?universe=uni-489f34dhj37sghj
chains_url: /chains?universe=uni-489f34dhj37sghj
If you are not familiar with the line
unevaluatedProperties: false
then please review Master JSON Schema’s Subtleties.
You may notice some potential for keeping our API design DRY. Both the newUniverse
and this universe
schemas have the same name
and description
properties. This is a fairly common situation, so let’s show how a common reuse refactoring solves it. We can thus establish a common schema definition pattern that allows for (end encourages) reuse. To restructure, we define a mutableUniverseFields
schema with the common fields and mix them into the newUniverse
and this universe
schemas:
components:
schemas:
mutableUniverseFields:
title: Mutable Universe Fields
description: Properties of a Chain Link Universe that are mutable.
properties:
name:
description: The name of the universe.
minLength: 4
maxLength: 64
description:
description: The description of the universe.
minLength: 4
maxLength: 512
newUniverse:
title: Request body to create a new universe.
description: >-
Properties to create a new universe in which authors can
create new characters and new chains.
type: object
required:
- name
- description
unevaluatedProperties: false
allOf:
- $ref: "#/components/schemas/mutableUniverseFields"
- properties:
sourceUniverse_url:
description:
An optional universe that this one is derived from.
type: string
format: uri_reference
example:
name: DragonTerr
description:
A world where dragons rule, but not without challengers.
universe:
title: Universe
description: >-
A universe in which authors can
create new characters and new chains.
type: object
required:
- id
- name
- description
- createdAt
- creator_url
- characters_url
- chains_url
unevaluatedProperties: false
allOf:
- $ref: "#/components/schemas/mutableUniverseFields"
- properties:
id:
$ref: '#/components/schemas/resourceId'
name:
description: The name of the universe.
minLength: 4
maxLength: 64
description:
description: The description of the universe.
minLength: 4
maxLength: 512
sourceUniverse_url:
description:
An optional universe that this one is derived from.
type: string
format: uri_reference
createdAt:
description: >-
The date and time the author created universe,
in RFC 3339 date-time format.
type: string
format: date-time
creator_url:
description: The URL of this universe's creator/author.
type: string
format: uri-reference
characters_url:
description: >-
The URL of the API operation to list the
characters that exist in this universe.
type: string
format: uri-reference
chains_url:
description: >-
The URL of the API operation to list the chains
that exist in this universe.
type: string
format: uri-reference
example:
id: uni-489f34dhj37sghj
name: DragonTerr
description:
A world where dragons rule, but not without challengers.
createdAt: '2023-08-23T18:34:10.444Z'
creator_url: /authors/au-ndklxhjf8933x0
characters_url: /characters?universe=uni-489f34dhj37sghj
chains_url: /chains?universe=uni-489f34dhj37sghj
This universe
schema is generic. Not only can we use it to define the application/json
response from our createUniverse
operation, it can also serve as the application/json
response from our getUniverse
operation.
The final step is to define the possible problem and error responses when creating a universe. These are done by defining additional mappings from HTTP response codes → OpenAPI response objects, as I discussed in Your API Has Problems. Deal With It.
paths:
/universes:
post:
operationId: createUniverse
summary: Create a new universe.
tags:
- Universes
description: Create a new universe in the collection of universes.
requestBody:
description: A new universe to create.
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/newUniverse'
responses:
'201':
description: Created. New universe created.
content:
application/json:
schema:
$ref: '#/components/schemas/universe'
headers:
Location:
description: The URL of the new resource.
schema:
type: string
format: uri-reference
'400':
description: >-
Bad Request. The request body was not well formed.
$ref: "#/components/responses/400"
'401':
$ref: "#/components/responses/401"
'403':
description: >-
Forbidden. The authenticated user is
not allowed to create universes.
$ref: "#/components/responses/403"
'409':
description: >-
Conflict. There is already a universe with the given name.
$ref: "#/components/responses/409"
'422':
description: >-
Unprocessable Entity.
Data in the request cannot be processed.
such as a `sourceUniverse_url` that does not
refer to an existing universe.
$ref: "#/components/responses/422"
'429':
$ref: "#/components/responses/429"
'4XX':
$ref: "#/components/responses/4XX"
'5XX':
$ref: "#/components/responses/5XX"
This covers the likely problems resulting from incorrect requests:
No authorization provided (401 Unauthorized)
Client not allowed to create a universe (403 Forbidden)
Request body is not valid JSON or the data does not match the
newUniverse
schema (400 Bad Request)A universe of the given name already exists (409 Conflict)
The reference to the base universe is an invalid Universe URL (422 Unprocessable Entity)
Too many requests — client has exceeded its rate limit (429 Too Many Requests)
Some other 4XX-level problem (4XX)
Any server-side error (5XX)
Observations
This article pulls in lots of aggregated knowledge from earlier articles in the Language of API Design series:
Mapping a domain model to OpenAPI
Patterns for clean URLs and URL structure
Defining the request body for a POST operation
Defining API responses with OpenAPI response objects and response body data format with JSON Schema
Composing JSON Schemas to keep the OpenAPI definition DRY
Understanding the subtleties of JSON schema (notably, unevaluatedProperties)
Defining reusable response objects to keep the OpenAPI definition DRY
Defining how the API handles problems with client requests as well as server errors
We’ll discuss API security in more depth really really soon. Hang in there!