What makes a great API

I recently did some work on a new ASP.NET Web API project using .Net Core. While doing research and development I could see some themes and patterns on What makes a great API and decided to collate this information.

This was for a RESTful (Representational State Transfer) Web based API (hypertext-driven) with JSON resonse, this was not the API of a new class interface. I dont believe being RESTful makes your API great, SOAP (Simple Object Access Protocol) could work just as well however REST is certainly more popular today. Have a look at SOAP vs. REST: A Look at Two Different API Styles.

For the URI format see HTTP verbs.

Pillars

Clear pillars are needed for a great API, this is certainly not an exhaustive list but these stood out for me.

Security

Dont bolt security on at the end, secure your API from the start.

  • Use OAuth 2.0 for authorization
  • Never put stack traces in responses, this can disclose sensitive information
  • Errors must be represented in Problem JSON (RFC 7807) format, providing error type (URI), and optionally title, status, detail along with others.

Supported & Well Architected

  • Use the OpenAPI specification
  • Later updates to the specification can be done at editor.swagger.io
  • Document the API first as a proposal and then implement the empty controllers and Generate the OpenAPI specification via Swashbuckle. Use the generated specification to test that the implementation respects the contract.
  • Write API documentation and build API client SDKs (Software development kit)
  • Utilize patterns, standards, templates, and frameworks to realize the features of an API which are not domain specific.

Consistent

  • Be stateless
  • have a common look and feel
  • The number of items returned by a collection must be limited and paginated (Sorting & Paging)
  • Fielding defined appropriate use within HTTP of the canonical verbs GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS. So don’t change state with GET :). See http-verbs and verb notes below.
  • Search by criteria, examples
    • /audits/date?start=&end=
    • /audits/search?q=foobar
  • HTTP header Fields should be in Hyphenated-Pascal-Case format
  • Date and Time values must be represented as YYYY-MM-DDThh:mm:ss[.sss]Z format strings.
  • Dont use null for any represented types
  • Resource endpoints must use plural resource forms, example /foos
  • Resources and sub-resources must be hierarchically identified, examples
    • /{resources}/[resource-id]/{sub-resources}/[sub-resource-id]
  • Use hyphens to to improve readability
    • GOOD: /{resources}/[resource-id]
    • BAD: /{resources}/[resourceId]
  • Always use lower case in URI paths
  • Resources naming
    • document, this is a single resource inside the collection. eg: /{resources}/ document would be resource
    • collection, this is a collection of documents. eg: /{resources}/
    • store, WAT
    • controller, this is a procedural concept / executable function with parameters. eg: /{resources}/{id}/some-thing-related-to-resource/archive (archive is the controller)

Verb Notes

GET

  • returns 200 when the resource or collection is found, the resource or collection is returned in the body
  • returns 404 and no body when nothing was found
  • should not have a payload, if you are in a situation where you need to send heaps of data as encoded query parameters then rather use a POST with content body
GET Examples
1
2
3
4
5
6
7
8
GET /users/12345                ~ filter in user 12345

RESPONSE
{
"id": 1,
"name": "Jane Doe",
"email": "john.doe@example.com"
}
1
2
3
4
5
6
7
8
9
10
GET /users?name=John%20Doe      ~ filter by name, this would return an array

RESPONSE
[
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GET /users                      ~ get all users
~ pagination would be sensible. ie: return 10 users at page 1

RESPONSE
[
{
"id": 1,
"name": "John Doe",
"email": "john.doe@example.com"
},
{
"id": 2,
"name": "Jane Doe",
"email": "jane.doe@example.com"
}
]
1
2
3
4
5
6
7
8
9
10
11
12
13
GET /users                     ~ example of how pagination can be added 
~ with new props `items` and `links`

RESPONSE
{
"items": [
...
],
"links": {
"next": "/users?page=2",
"previous": "/users?page=1"
}
}

PUT

  • used to do a resource update, complete content is provided
  • sometimes partial resource is provided (here you should consider a PATCH)
  • returns 200 if the update was completed and no body, generally rather return 204 No Content
  • returns 201 if something was created (here you should consider a POST)
PUT Examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PUT /users/12345
Content-Type: application/json
Body:
{
"id": 12345,
"name": "John Doe",
"email": "john.doe@example.com"
}

RESPONSE (if you return a body)
{
"id": 12345,
"name": "Jane Doe",
"email": "john.doe@example.com"
}

This request will replace the entire user resource with the new data.

POST

  • used to create a single complete resource, not idempotent
  • returns 200 if successful, the resource or collection is returned in the body
  • returns 201 if something was created, the resource or collection is returned in the body
  • returns 202 if the request was accepted and will be completed later
  • returns 204 with Location header if the resource is not returned
POST Examples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST /users
Content-Type: application/json
Body:
{
"name": "John Doe",
"email": "john.doe@example.com"
}

RESPONSE (if you return a body)
{
"id": 12346,
"name": "Jane Doe",
"email": "jane.doe@example.com"
}

PATCH

  • used to do a resource update, partial content is provided

The main difference between HTTP PATCH and PUT is that PATCH is used for partial updates to a resource, while PUT is used for full updates. This means that with PATCH, you can send only the data that you want to update, without having to send the entire resource again. With PUT, you must send the entire resource, even if you only want to update a small part of it.

PATCH Examples
1
2
3
4
5
6
7
8
9
10
11
12
13
PATCH /users/12345
Content-Type: application/json
Body:
{
"name": "John Doe"
}

RESPONSE (if you return a body)
{
"id": 12345,
"name": "Jane Doe",
"email": "john.doe@example.com"
}

This request will update the user’s name to “John Doe”, without modifying any of their other data.

DELETE

  • used to delete a resource
  • returns 200 with the deleted resource in the body
  • returns 204 if you dont wish to return the deleted resource
  • returns 404 if the resource you wanted to delete was not found
  • returns 410 if the resource was already deleted
DELETE Examples
1
2
3
4
DELETE /users/12345

RESPONSE (if you are not returning a body)
HTTP/1.1 204 No Content

HEAD

  • used to retrieve header information, often used by web crawlers to check the availability and last modification date of resources before downloading them
  • its the same as a GET but only returns header and never a body
HEAD Examples
1
2
3
4
5
6
7
HEAD /index.html

RESPONSE
HTTP/1.1 200 OK
Content-Type: text/html
Last-Modified: Tue, 29 Aug 2023 15:55:18 GMT
Content-Length: 1024

OPTIONS

  • used to inspect the avalible operations (so the HTTP verbs) of a given endpoint
OPTIONS Examples
1
OPTIONS /users HTTP/1.1
1
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS

Performance

  • Allow to scale horizontally by using Docker containers and orchestration software like Kubernetes
  • Rate limit client requests using HTTP Status Code 429 Too Many Requests

Ability to change

  • Avoid breaking changes, dont remove fields/methods/add additional validations

References