Course outline · 0% complete

0/25 lessons0%

Course overview →

POSTing data and JSON APIs

lesson 5-3 · ~12 min · 16/25

Sending a body with curl

Reading is GET. To send data, use POST with a body, which in modern APIs is almost always JSON, the {"key": "value"} data format from lesson 4-4. With curl:

curl -X POST https://api.example.com/notes \
  -H "Content-Type: application/json" \
  -d '{"text": "buy milk", "done": false}'

Three new flags:

  • -X POST sets the method (lesson 4-3).
  • -H adds a request header. Content-Type: application/json tells the server how to parse the body (the same header you read out of a response in lesson 4-2, now going the other way).
  • -d supplies the body. curl automatically computes Content-Length for you.

Many APIs also require proof of who you are, usually another header: -H "Authorization: Bearer YOUR_TOKEN".

A recorded session

A guided session against a practice JSON API. The status codes follow the lesson 4-3 rules exactly.

Each step below shows the command and the output it printed.

Step 1. Create a note with POST. The API answers 201 Created and echoes the stored object back, now with an id.

$ curl -i -X POST https://api.example.test/notes -H "Content-Type: application/json" -d '{"text": "buy milk", "done": false}'
HTTP/1.1 201 Created
Content-Type: application/json

{"id": 17, "text": "buy milk", "done": false}

Step 2. Read it back with a plain GET using its id in the path.

$ curl https://api.example.test/notes/17
{"id": 17, "text": "buy milk", "done": false}

Step 3. Send broken JSON (note the missing closing brace) and the server answers 400 Bad Request: a 4xx, your fault, exactly as lesson 4-3 promised.

$ curl -i -X POST https://api.example.test/notes -H "Content-Type: application/json" -d '{"text": "oops"'
HTTP/1.1 400 Bad Request
Content-Type: application/json

{"error": "invalid JSON body"}

Step 4. Delete it with the DELETE method. 204 No Content is a success with an empty body.

$ curl -i -X DELETE https://api.example.test/notes/17
HTTP/1.1 204 No Content

Building and reading a JSON body in code

json.dumps produces the exact body text you would pass to curl -d, and json.loads parses a response body back into data.

import json

body = json.dumps({"text": "buy milk", "done": False})
print("request body:", body)

response = '{"id": 17, "text": "buy milk", "done": false}'
data = json.loads(response)
print("created note", data["id"], "->", data["text"])

Output

request body: {"text": "buy milk", "done": false}
created note 17 -> buy milk

Python's False became JSON's false on the way out, the same translation as lesson 4-4 in reverse. This is why you build bodies with a serializer rather than with string formatting.

The response carries an id that the request never sent. The server assigned it, which is the normal division of labor for a create: the client supplies the content and the server supplies the identity.

Reading a nested response

The repos live in a list under the "repos" key, so the loop walks that list.

import json

response = '{"user": "ada", "repos": [{"name": "engine", "stars": 412}, {"name": "notes", "stars": 7}]}'

data = json.loads(response)
for repo in data["repos"]:
    print(repo["name"], "has", repo["stars"], "stars")

Output

engine has 412 stars
notes has 7 stars

Reading the code

  • data = json.loads(response) gives a dict, and every access after that is ordinary Python.
  • data["repos"] is a list of dicts, so for repo in data["repos"]: visits each one in turn.
  • Each repo exposes repo["name"] and repo["stars"], which is one dict lookup per field.
  • This dict-inside-list-inside-dict shape is what almost every real API response looks like. Once you can read two levels, the depth stops mattering, because each level is either a key lookup or a loop.

The header that declares a JSON body

The header is Content-Type: application/json.

Servers do not guess the body format, they read this header. Without it, many frameworks treat the body as plain text or form data, fail to parse it, and answer 400 Bad Request.

It is the same header you read out of responses in lesson 4-2, now traveling in the other direction, and with curl you set it with -H "Content-Type: application/json".

The failure is confusing precisely because the body was fine. A valid JSON payload plus a missing or wrong Content-Type produces the same 400 as malformed JSON, so checking the header is worth doing before you start staring at the body.