6 Faux Pas of HTTP API Design

HTTP APIs are very loose by definition: there’s no standards body, no API validator and not always someone there to tell you when you may be doing something incorrect. At ReadMe we have lots of experience in building, consuming and viewing APIs created by others. We even have our own open source API Explorer which is used by all of our projects to try out APIs in the browser.

I asked engineers in the ReadMe team, “What are some common issues or pitfalls you see when using APIs?” This is a definitely a non-exhaustive list, but avoiding some of these pitfalls will make your APIs more “standard,” intuitive, and easy to use. We came up with the following faux pas:

1. Sending a Mixture of Query Params and Body Params

When you create an API endpoint you should consider accepting either query parameters or body parameters, but not both. Since GET requests can’t have bodies, the best way to pass in parameters and filters is via query string. For POST requests (and other non-idempotent methods) you should prefer bodies.

This is an example of using both query and body params, for a POST request:

 


POST /employees?position=engineer HTTP/1.1
Host: api.example.com
Accept: */*
Content-Type: application/json
Content-Length: 25

{ "name": "Alice" }

It should be modified to send both properties via the body:


POST /employees HTTP/1.1
Host: api.example.com
Accept: */*
Content-Type: application/json
Content-Length: 43

{ "name": "Alice", "position": "engineer" }

Using query strings is fine for filtering, like so:


GET /employees?position=engineer HTTP/1.1
Host: example.com
Accept: */*

2. Sending Private API Keys in a Query String

You should never accept private API keys or passwords in query strings. Servers often print out the whole URL for debugging later in their log files. You don’t want your users’ passwords being stored in plain text on the disk.


GET /employees?apiKey=wWERfeUzeA3CuJRKtzfsMTiangi HTTP/1.1
Host: example.com

You can switch to using a header for this value to keep your users’ confidential keys and passwords safe:


GET /employees HTTP/1.1
Host: example.com
Authorization: wWERfeUzeA3CuJRKtzfsMTiangi

3. Using the Wrong Method: POST When You Mean PUT/PATCH

HTTP consists of 9 verbs, the most common of which are: GET, POST, PUT, PATCH and DELETE. In REST APIs, each of these roughly translates to the four functions of storage: CRUD.

ActionHTTP Verb
CreatePOST
ReadGET
UpdatePUT/PATCH
DeleteDELETE

Using the wrong verb is considered bad practice when creating an API, though it’s technically possible. As an API consumer, you would not expect a “Read” operation (GET) to perform a “Create” (POST). For example: Using GET to “Create” a new item.


GET /employees?name=Alice&position=engineer HTTP/1.1
Host: example.com

This should use POST, with a body.

4. Adding Verbs in URLs

We’ve seen this one lots of times, where a path contains a verb that should be represented via a method.


GET /createEmployee?name=Alice&position=engineer HTTP/1.1
Host: example.com

Example of a path containing the verb create.

This is similar to using the wrong HTTP method. To correct this, remove the verb from the path and use the correct method. (See table above.)

5. Accepting Multiple Types of Object on the Same URL

Sometimes we see developers that create the “one endpoint to rule them all™”—one endpoint that can take a whole host of object shapes. If a path has a very generic noun, like /entities and it accepts lots of different object “types,” like this:


{ "type": "employee", "name": "Alice" }

{ "type": "widget", "name": "Whosit" }

{ "type": "product", "name": "Book" }

It’s better to have a different endpoint for each entity:

  • /employees
  • /widgets
  • /products

This is easier for documentation purposes, easier for your debugging and simpler for your users.

6. Creating Your Own Version of Something, When a Header Exists

Often we also see people re-implementing something that already exists as part of the HTTP spec. The main example of this we see regards the Accept/Content-Type headers.

The Accept header is used by the client to let the API know what content type they would like to get back, the Content-Type header is used by the server to let the client know what type they are actually returning. This process is called content negotiation.

We’ve seen unnecessary workarounds for things Accept headers can already handle, such as:

  • ?format=json querystring.
  • .json mock extension on the end of the path.

There are lots of Accept-* headers that can be utilized for various ways of defining what you as a client expect or prefer:


As with every set of rules or best practices, there are times when they are meant to be broken. Typically if you intend for your API to be accessed directly via a browser, you may have to expose things via query string that you (in an ideal world) should do via a header. This is one of the benefits of REST/HTTP—they allow the flexibility to do whatever you want to do.

Are there any others that you’d add to this list? Am I completely wrong on this? Tweet @readme with your comments!