How to buildOctober 11, 20269 min read
Spring BootStep 1 of 7

HTTP and JSON: requests, responses, headers and data format

In short

An HTTP request is a text message made of a method, a target, headers and an optional body; a response is a status code, headers and a body. JSON is the format the body usually carries: objects, arrays, double-quoted strings, numbers, true, false and null. To work with APIs confidently you need three things: what a method means (is it safe, can it be retried), what the status code class says, and which Content-Type describes the body.

FundamentalsLevel: BeginnerHTTP & JSON

After this you will be able to

  • Break an HTTP request and response down into method, target, headers, body and status code
  • Explain which methods are safe and idempotent under RFC 9110 and why a POST must not be retried blindly
  • Send GET and POST with JSON via curl and fetch and check the response status, not just the fact that a response arrived
  • Find the cause of a JSON.parse error and predict what JSON.stringify drops during serialization

Verified with

Node.js 24 LTS (24.18)curl 8.21HTTP/1.1JSON (RFC 8259)

Checked against docs: October 11, 2026

Contents
  1. 01What a request and a response are made of
  2. 02Methods: safety and idempotency
  3. 03Status codes and headers worth knowing
  4. 04Hands-on: your own API on node:http, requests via curl and fetch
  5. 05JSON: syntax, JSON.parse and JSON.stringify
  6. 06Requests in DevTools and CORS in two paragraphs

What a request and a response are made of

In the lesson on how a website works, a request was the “waiter” between the browser and the server. Here we look inside: what exactly the waiter carries. HTTP (HyperText Transfer Protocol) is a message exchange protocol: the client sends a request and the server returns a response. The client can be a browser, a mobile app, another server or curl in a terminal.

In HTTP/1.1 a request is plain text. The first line holds the method, the request target and the protocol version. Then come headers — “name: value” pairs, one per line. Then an empty line, and after it an optional body. This is a request that creates an order:

HTTP
POST /api/orders HTTP/1.1
Host: 127.0.0.1:8080
Content-Type: application/json
Content-Length: 27

{"item": "latte", "qty": 1}
HTTP request with a JSON body: start line, headers, empty line, body

A response has the same shape with a different first line: protocol version, a three-digit status code and a short reason phrase, such as HTTP/1.1 201 Created. The phrase is informational only — programs rely on the number.

Methods: safety and idempotency

The method tells the server what the client wants. RFC 9110 defines two properties that drive the behaviour of browsers, proxies and your own code. A safe method is essentially read-only and does not ask the server to change anything. An idempotent method leaves the server in the same state no matter how many times an identical request is repeated.

GET

Purpose
Retrieve a representation of a resource
Safe
Yes
Idempotent
Yes

HEAD

Purpose
Same as GET, headers only
Safe
Yes
Idempotent
Yes

POST

Purpose
Submit data for processing, create a record
Safe
No
Idempotent
No

PUT

Purpose
Create or fully replace the resource at a URL
Safe
No
Idempotent
Yes

PATCH

Purpose
Partially modify a resource (RFC 5789)
Safe
No
Idempotent
No

DELETE

Purpose
Delete a resource
Safe
No
Idempotent
Yes

Idempotency is a practical property, not theory. RFC 9110 spells it out: if the connection drops before the response arrives, the client may retry a PUT or DELETE because repeating it will not change the outcome. A POST is different: a retry creates a second order or charges the card twice. That is why payment APIs add their own duplicate protection to POST, and forms block double submission.

Status codes and headers worth knowing

A status code is a three-digit number from 100 to 599. The first digit defines the class, and RFC 9110 requires a client to understand at least the class: an unknown 471 must be treated as a 400. The rule for your own code follows: check the class first, then specific values.

2xx

Meaning
Success: received, understood and accepted
Where you see it
200 OK — data in the body; 201 Created — record created, URL in Location; 204 No Content — success without a body

3xx

Meaning
Further action needed
Where you see it
301, 302, 308 — the resource lives at another URL

400

Meaning
The server cannot process the request due to a client error
Where you see it
The body is not valid JSON

401

Meaning
No valid credentials
Where you see it
Token missing or expired

404

Meaning
Resource not found
Where you see it
No order with that id

415

Meaning
Body format not supported
Where you see it
JSON sent without Content-Type: application/json

422

Meaning
Syntax is fine, but the data breaks the rules
Where you see it
Quantity is zero

5xx

Meaning
The server failed to fulfil a valid request
Where you see it
500 — an unhandled error in server code

Headers describe the message, not the data inside it. Header names are case-insensitive: Content-Type and content-type are the same. Node.js lowercases all names in req.headers, so server code reads req.headers["content-type"].

  • Content-Type — the body format: application/json, text/html; charset=utf-8. Without it, RFC 9110 lets the recipient treat the body as raw bytes or sniff the type — do not rely on that.
  • Content-Length — body size in bytes, not characters: a Cyrillic letter takes two bytes in UTF-8.
  • Authorization — credentials as “scheme value”, for example Bearer YOUR_TOKEN. Anyone holding such a token can use it, so send it over HTTPS only.
  • Cache-Control — caching rules. no-store forbids storing the response, no-cache allows storing but requires revalidation before every reuse, max-age=N keeps the response fresh for N seconds.
  • Location — the URL of the created resource in a 201 response, or the new URL in a redirect.

Hands-on: your own API on node:http, requests via curl and fetch

The best way to see HTTP is to run a tiny server and send it requests. All you need is Node.js 24: the node:http module is built in, no external packages. The server keeps orders in memory, returns an order on GET and creates one on POST, checking the header, the JSON syntax and the data itself — each check maps to its own status code.

server.mjs
import { createServer } from "node:http";

const orders = new Map([[1, { id: 1, item: "cappuccino", qty: 2 }]]);
let nextId = 2;

function sendJson(res, status, data, headers = {}) {
  const body = JSON.stringify(data);
  res.writeHead(status, {
    "Content-Type": "application/json; charset=utf-8",
    "Content-Length": Buffer.byteLength(body),
    ...headers,
  });
  res.end(body);
}

async function readBody(req) {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  return Buffer.concat(chunks).toString("utf8");
}

const server = createServer(async (req, res) => {
  const url = new URL(req.url, "http://localhost");
  console.log(req.method, url.pathname, req.headers["content-type"] ?? "-");

  const match = url.pathname.match(/^\/api\/orders\/(\d+)$/);
  if (req.method === "GET" && match) {
    const order = orders.get(Number(match[1]));
    if (!order) return sendJson(res, 404, { error: "Order not found" });
    return sendJson(res, 200, order, { "Cache-Control": "no-store" });
  }

  if (req.method === "POST" && url.pathname === "/api/orders") {
    if (!req.headers["content-type"]?.startsWith("application/json")) {
      return sendJson(res, 415, { error: "Expected Content-Type: application/json" });
    }
    let input;
    try {
      input = JSON.parse(await readBody(req));
    } catch {
      return sendJson(res, 400, { error: "Request body is not valid JSON" });
    }
    if (typeof input?.item !== "string" || !Number.isInteger(input.qty) || input.qty < 1) {
      return sendJson(res, 422, { error: "Fields required: item (string), qty (integer >= 1)" });
    }
    const order = { id: nextId++, item: input.item, qty: input.qty };
    orders.set(order.id, order);
    return sendJson(res, 201, order, { Location: `/api/orders/${order.id}` });
  }

  sendJson(res, 404, { error: "Not found" });
});

server.listen(8080, "127.0.0.1", () => {
  console.log("API: http://127.0.0.1:8080");
});
Teaching server: in-memory data lost on restart, no authentication — not for production

Run node server.mjs and open a second terminal. The curl flag -s hides the progress meter, -i prints the status line and response headers, -X sets the method, -H adds a header, -d sends a body. The commands are for bash (macOS, Linux, Git Bash on Windows): PowerShell quotes differently. The output is real, from our run:

terminal
$ curl -si http://127.0.0.1:8080/api/orders/1
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 36
Cache-Control: no-store
Date: Sun, 11 Oct 2026 21:56:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":1,"item":"cappuccino","qty":2}

$ curl -si -X POST http://127.0.0.1:8080/api/orders -H 'Content-Type: application/json' -d '{"item": "latte", "qty": 1}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Content-Length: 31
Location: /api/orders/2
Date: Sun, 11 Oct 2026 21:56:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"id":2,"item":"latte","qty":1}

$ curl -si -X POST http://127.0.0.1:8080/api/orders -H 'Content-Type: application/json' -d "{'item': 'latte'}"
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
Content-Length: 42
Date: Sun, 11 Oct 2026 21:56:00 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"error":"Request body is not valid JSON"}

$ curl -si -X POST http://127.0.0.1:8080/api/orders -d '{"item": "latte", "qty": 1}'
HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json; charset=utf-8
Content-Length: 51
Date: Sun, 11 Oct 2026 21:56:01 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"error":"Expected Content-Type: application/json"}
Four requests — four different responses: 200, 201, 400 and 415

Look at the last request. Without -H, curl sets Content-Type: application/x-www-form-urlencoded on its own — that is what our server log recorded. The body still contains JSON, but the server rightly trusts the header and answers 415. This is one of the most common reasons behind “the server doesn't see my data”.

Now the same from code. fetch has been built into Node.js since version 18 and stable since 21; it behaves the same in the browser. Restart the server so order ids start over, and run the client:

client.mjs
const API = "http://127.0.0.1:8080/api";

async function request(method, path, body) {
  const res = await fetch(API + path, {
    method,
    headers: body === undefined ? {} : { "Content-Type": "application/json" },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const data = await res.json();
  console.log(method, path, "→", res.status, res.ok ? "ok" : "error", data);
  return data;
}

await request("GET", "/orders/1");
const created = await request("POST", "/orders", { item: "latte", qty: 1 });
await request("GET", `/orders/${created.id}`);
await request("POST", "/orders", { item: "latte", qty: 0 });
await request("GET", "/orders/999");
fetch client: the body becomes JSON via JSON.stringify, the response is parsed with res.json()
terminal
$ node client.mjs
GET /orders/1 → 200 ok { id: 1, item: 'cappuccino', qty: 2 }
POST /orders → 201 ok { id: 2, item: 'latte', qty: 1 }
GET /orders/2 → 200 ok { id: 2, item: 'latte', qty: 1 }
POST /orders → 422 error { error: 'Fields required: item (string), qty (integer >= 1)' }
GET /orders/999 → 404 error { error: 'Order not found' }
Real output: fetch does not throw on 404 or 422

JSON: syntax, JSON.parse and JSON.stringify

JSON (JavaScript Object Notation) is defined in RFC 8259. A JSON value is an object, an array, a number, a string or one of three literals: true, false, null. The rules are stricter than JavaScript: strings and member names use double quotes only, no trailing commas, no comments, no leading zeros in numbers, and NaN and Infinity are not allowed. Between systems JSON travels as UTF-8, and the media type is application/json.

JavaScript has two functions for JSON. JSON.stringify turns a value into a string for sending, JSON.parse turns a string back into a value. Both can silently drop data or throw. This script collects the typical traps; below is real Node.js 24 output:

json-pitfalls.mjs
const order = {
  id: 7,
  createdAt: new Date("2026-10-11T09:30:00Z"),
  comment: undefined,
  discount: NaN,
  format() {},
};
console.log(JSON.stringify(order));
console.log(JSON.stringify({ items: ["latte", undefined] }));
console.log(JSON.stringify({ id: 1, tags: ["new"] }, null, 2));

const broken = [
  "{'item': 'latte'}",
  '{"item": "latte",}',
  '{"qty": 012}',
  "",
];
for (const text of broken) {
  try {
    JSON.parse(text);
  } catch (error) {
    console.log(error.name + ":", error.message);
  }
}

console.log(JSON.parse('{"id": 9007199254740993}').id);

try {
  JSON.stringify({ total: 10n });
} catch (error) {
  console.log(error.name + ":", error.message);
}

const a = { name: "a" };
a.self = a;
try {
  JSON.stringify(a);
} catch (error) {
  console.log(error.name + ":", error.message.split("\n")[0]);
}
What JSON.stringify drops and where JSON.parse fails
terminal
$ node json-pitfalls.mjs
{"id":7,"createdAt":"2026-10-11T09:30:00.000Z","discount":null}
{"items":["latte",null]}
{
  "id": 1,
  "tags": [
    "new"
  ]
}
SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)
SyntaxError: Expected double-quoted property name in JSON at position 17 (line 1 column 18)
SyntaxError: Unexpected number in JSON at position 9 (line 1 column 10)
SyntaxError: Unexpected end of JSON input
9007199254740992
TypeError: Do not know how to serialize a BigInt
TypeError: Converting circular structure to JSON
Error texts come from V8 in Node.js 24; Firefox and Safari word them differently

comment and format are gone

Why
undefined and functions are skipped in objects and become null in arrays
What to do
Send null explicitly if the field must be present

discount became null

Why
NaN and Infinity are not valid JSON and serialize as null
What to do
Validate numbers before sending

The date became a string

Why
Date has a toJSON method that returns an ISO 8601 string
What to do
Convert the string back to a Date yourself when parsing

SyntaxError while parsing

Why
Single quotes, a trailing comma, a leading zero or an empty string
What to do
Wrap JSON.parse in try/catch and answer 400

9007199254740993 became …992

Why
JSON numbers are parsed into 64-bit floats; RFC 8259 guarantees exact integers only up to 2^53 − 1
What to do
Send large ids and money amounts as strings

TypeError for BigInt and a circular reference

Why
There is no serialization rule for them
What to do
Convert the value beforehand or use a replacer

Requests in DevTools and CORS in two paragraphs

  1. Open the Network panel

    In Chrome or Edge press F12, then Network. Reload the page or click the button that sends the request. The Fetch/XHR filter keeps only API requests.

  2. Look at the headers

    Click a request. The Headers tab shows the URL, method, status code, and request and response headers.

  3. Look at the body

    Payload is what the client sent, Response is the raw server reply, Preview is the same reply in a friendlier view.

  4. Replay it in a terminal

    Right-click the request → Copy → Copy as cURL. You get a curl command with the same headers: handy for reproducing a bug outside the browser and handing it to a backend developer.

If the same request works in curl but fails in the browser with a console error, it is almost certainly CORS (Cross-Origin Resource Sharing). Under the same-origin policy the browser does not let a script read responses from another domain, port or scheme unless the server explicitly allows it with Access-Control-Allow-Origin. Curl and server-side code are not subject to this policy — it protects the user in the browser and is not a property of HTTP itself.

For “non-simple” requests — for example, a POST with Content-Type: application/json or an Authorization header — the browser first sends an OPTIONS preflight asking whether that method and those headers are allowed. JavaScript does not get the details of a CORS failure: for security reasons they are visible only in the browser console. CORS is fixed on the server, not in the frontend.

  • The method matches the action: GET for reads, POST for creation, no data changes via GET
  • A request with a body has the right Content-Type
  • Code checks res.ok or res.status before reading the body
  • JSON.parse of incoming data is wrapped in try/catch
  • Large numbers and money travel as strings
  • Tokens and keys go only in the Authorization header, only over HTTPS and never in frontend code
How this was verified5 · passed1 · not runchecked 11 October 2026

“Run” means the code was executed in a scratch project or container and the output in the article comes from that run. “Partial” means it was checked against stubs instead of external services. “Not run” means it needs a device, an account or a key; the steps were checked against the official documentation. The verification project files and run logs are kept in the InterLAB archive.

Tell us about your task — we'll review your request and get back to you

Got a project?
We'll scope and estimate it — for free.

Tell us about the product or the problem. After reviewing the task we'll come back with an architecture, a timeline and a price for the first version.

  • Scope and risks estimated before we start
  • NDA — yours or ours
  • Work starts within a week

Please do not send passwords, access keys or source code through this form — we will agree a safe way to share access separately.

By clicking the button you agree to the processing of your personal data.