Request body encoding: JSON? x-www-form-urlencoded?
Tags: api, webdev, beginners, learning
Published: 2019-09-28 02:40:07

I'm planning a new API and was curious about what kind of requests I should accept.

The most common [Content-Types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type) I've seen for popular modern REST APIs include:
* `application/x-www-form-urlencoded`
* `application/json`

Shout-out to `application/graphql` too, but this article will focus on the two above :)

Of course, when just starting out it's easy to accept both and forget about it. For example...

```js
import express from 'express';
import bodyParser from 'body-parser';

const app = express();

// application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({extended: false}));

// application/json
app.use(bodyParser.json());

export default app;
```
<figcaption>A minimal example of a Node.js webserver accepting both request body types</figcaption>

I've done this many times in the past. However, I wanted to set out and discover the true pros and cons of each. I wanted to understand *why* someone would choose one over the other.

# Initial bias

Before I started my search, I wanted to keep my biases in check. What do I currently believe and what am I probably predisposed to want to validate?

### `application/json` is beginner-friendly...mostly

[`axios`](https://www.npmjs.com/package/axios) and [`superagent`](https://www.npmjs.com/package/superagent), two of the more popular npm HTTP libraries, work with JSON bodies by default. To send as `application/x-www-form-urlencoded`, you have to add additional configuration options.

For example, here's how you might do it with axios. Leveraging a querystring encoding library is necessary:

```js
import axios from 'axios';
import qs from 'qs';

axios({
  method: 'POST',
  url: '/resource',
  data: qs.stringify({
    foo: 'bar',
  }),
});
```

It's not really a significant amount of boilerplate, but it's something I've run into in the past.

On the other hand, tools like [Postman](https://www.getpostman.com/) are much easier to work with when using `application/x-www-form-urlencoded`:

![Postman urlencoded demo](https://thepracticaldev.s3.amazonaws.com/i/fqp0v7i4igiu7j80iztv.gif)
<figcaption>Toggling and descriptions? Fantastic!</figcaption>

Whereas `application/json` doesn't allow toggling nor describing individual keys:

![Postman json demo](https://thepracticaldev.s3.amazonaws.com/i/zlqat0suweogytl2z9ij.gif)
<figcaption>Yeah yeah, I know. Comments aren't valid JSON anyway. But come on!</figcaption>

### URL encoding is used by some of the best APIs out there

I commonly see Stripe referred to as [the gold standard](https://www.google.com/search?q=%22gold+standard%22+stripe+api) as far as API quality. Guess what encoding they use?

> The Stripe API is organized around REST. Our API has predictable resource-oriented URLs, **accepts form-encoded request bodies**, returns JSON-encoded responses, and uses standard HTTP response codes, authentication, and verbs
>
> -- [Stripe Docs](https://stripe.com/docs/api)

And it's not just them. Twilio, the market leader for SMS, is on the same page:

> Creating or updating a resource involves performing an HTTP PUT or HTTP POST to a resource URI. In the PUT or POST, you represent the properties of the object you wish to update as form urlencoded key/value pairs. Don't worry, this is already the way browsers encode POSTs by default. **But be sure to set the HTTP Content-Type header to "application/x-www-form-urlencoded" for your requests if you are writing your own client.**
>
> -- [Twilio Docs](https://www.twilio.com/docs/usage/your-request-to-twilio#post)

These folks have got to know what they're doing, right?

---------

Okay, so some positive and negative biases in regards to both approaches. At this point, I started Googling around. My main search terms:
* `application/json vs application/x-www-form-urlencoded`
* `json vs form`
  * `differences`
  * `performance`
  * `best practices`

# What I learned

### Preflight OPTIONS requests are always sent with JSON

Essentially, there's [a whitelist](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#Simple_requests) of request properties and values. As long as your HTTP requests stay within this whitelist, no preflight requests are sent.

The `application/json` Content-Type isn't on the list, so using this for requests will result in preflight requests. You can read more about this below:

{% link https://dev.to/effingkay/cors-preflighted-requests--options-method-3024#simple-requests %}

The whitelist is fairly restrictive. No `Authorization` headers, `Cache-Control`, `Keep-Alive`, etc. I'd recommend not trying to circumvent CORS in this regard unless you find there's a significant performance opportunity and minimal additional code complexity.

### URL encoded arrays can be a nightmare

Sending any kind of complex data with `application/x-www-form-urlencoded` has historically been...not ideal.

The following are two patterns I've seen for sending an array called `a` with members `b` and `c`. Neither of them are particularly great:
* `a[]=b&a[]=c`
* `a[0]=b&a[1]=c`

For larger payloads, you may be [saving bytes with JSON](https://stackoverflow.com/a/26527335/6609109) as well.

# tl;dr

If you can do so securely, support both content types. Always sanitize user input, don't bite off more than you can chew, etc.

If you must choose *one* request body for your API, go with whatever works for your use case.
* For complex data (especially array/nested object parameters) or if you already return JSON and want to be consistent: consider `application/json`.
* For mostly flat param trees, `application/x-www-form-urlencoded` is tried and tested. After all, it's the default for browsers! 

# Further reading

https://m.alphasights.com/killing-cors-preflight-requests-on-a-react-spa-1f9b04aa5730