Almost everything you do on the web — open a page, submit a form, call someone's API — is an exchange of HTTP requests and responses. HTTP (HyperText Transfer Protocol) is a simple text dialogue between a client and a server: the client says "give me this" or "save this", the server answers "here you go" or "can't, here's why". There's no magic: once you understand the anatomy of a single request and a single response, the whole protocol makes sense.
The good news is that HTTP is human-readable. You can print a request and a response and read them like a letter: at the top what's being asked, below the terms, at the end the content. Let's break down that structure part by part, then look at methods, status codes, and headers — three things a backend developer uses every single day.
Anatomy of a request and a response
Any HTTP request consists of four parts in strict order: the start line, the headers, an empty line, and (optionally) the body.
Here's what a full request looks like:
POST /orders HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGc...
{"productId": 42, "quantity": 2}
Line by line:
- Start line
POST /orders HTTP/1.1— what we're doing (methodPOST), with what (path/orders), and over which protocol version. - Headers — "name: value" pairs, the request metadata: which host to look for the resource on, what format the body is, who we are.
- Empty line — the boundary separating headers from the body. Without it the server can't tell where the metadata ends.
- Body — the data itself.
GETusually has none;POST/PUTdo.
The response mirrors the same shape:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /orders/1001
{"id": 1001, "status": "created"}
The response start line carries a status code (201) and its text label (Created). Then the same headers, empty line, and body. That's the whole protocol: a dialogue of messages like these.
Methods: what we want to do
The method is the verb of the request; it tells the server the intent. There are five main ones:
- GET — "give me the resource". A read, changes nothing. Requesting a list of orders, a single page, an image — all
GET. - POST — "create something new" or "perform an action". Submitting a form, creating an order. Each call usually creates a new entity.
- PUT — "replace the resource entirely" with this content. If it doesn't exist, it's created; if it does, it's fully overwritten.
- PATCH — "change the resource partially". Send only the fields that changed, not the whole object.
- DELETE — "delete the resource".
A method is an agreement about meaning, not a technical constraint. Technically you could hide a deletion behind a GET, but that breaks every expectation: browsers, proxies, and search engines treat GET as a safe read and may call it whenever they like. So the verb is chosen by the meaning of the operation.
Safe and idempotent methods
Two properties of methods that sound academic but solve a very practical problem — what happens if the request is repeated.
A safe method changes nothing on the server. GET is safe: request a page as many times as you like, the server's state won't budge. That's why a browser happily does a GET when you follow a link, and a proxy caches it.
An idempotent method can be repeated any number of times, and the result stays the same as after the first time. PUT is idempotent: "set the address to X" — once or five times, the address ends up X. DELETE too: delete once, and a repeat request finds the resource already gone — state unchanged. But POST is not idempotent: three POST /orders create three orders.
Why this matters for the backend. Networks are unreliable: a response can get lost on the way, and the client, not seeing it, repeats the request. If the method is idempotent, the repeat is safe — nothing gets duplicated. If it isn't (like POST), the repeat may create a duplicate, and you have to guard against that separately — with an idempotency key, which lets the server recognize a repeated attempt and not create a second entity.
| Method | Safe | Idempotent |
|---|---|---|
| GET | yes | yes |
| PUT | no | yes |
| DELETE | no | yes |
| POST | no | no |
| PATCH | no | no |
Status codes: what the server answered
A status code is a three-digit number in the response, split into five classes by the first digit. The class already tells the main story, and the specific number refines it.
2xx — success. All good.
200 OK— the standard successful response, the body holds the result.201 Created— a new resource was created (the typical answer to aPOST); theLocationheader holds its address.
3xx — redirection. The resource isn't here, go elsewhere.
301 Moved Permanently— moved for good, remember the new address.
4xx — client error. The sender is at fault: asked for the wrong thing, formatted it wrong.
400 Bad Request— the request is malformed: invalid JSON, a missing field.401 Unauthorized— you didn't identify yourself, authentication is required (who are you?).403 Forbidden— you identified yourself, but you don't have the rights (I know who you are, but no).404 Not Found— there's no such resource.409 Conflict— a conflict with the current state: for example, creating something that already exists.
5xx — server error. The client did everything right; something broke on the server side.
500 Internal Server Error— something failed inside, an unhandled exception.502 Bad Gateway— the server acted as an intermediary and got a garbled answer from whoever it contacted.503 Service Unavailable— the service is temporarily down (overload, maintenance).
The practical difference between 4xx and 5xx is huge. 4xx — "fix your request, repeating it with the same body won't help". 5xx — "trouble on my side, try later" — these are the responses that often make sense to retry with a delay.
Key headers
Headers are the terms of the deal, the metadata around the body. There are dozens, but a handful is enough to start:
- Content-Type — the format of the body:
application/json,text/html,image/png. It tells the receiver how to parse the content. - Authorization — credentials:
Bearer <token>. This is how the client proves who it is. - Cache-Control — caching rules:
no-store(don't keep it at all),max-age=3600(may hold for an hour). It governs whether browsers and proxies re-ask the server or hand back a stored copy. - Location — where to look: the address of a new resource on
201, or the new address on a3xx.
Headers appear in both the request and the response, and some come in pairs: the client says Accept: application/json ("I want JSON"), the server answers Content-Type: application/json ("here's JSON").
HTTP is stateless: each request stands alone
An important property: HTTP is stateless. The server doesn't remember the previous request — each message is self-contained and carries everything needed to process it. The second request doesn't know the first ever happened.
An analogy is talking to an operator who loses their memory after every sentence. For them to understand you, each line has to name yourself and the matter again. That's exactly why Authorization is sent with every request: the server doesn't remember that you already identified yourself a minute ago.
For the backend this is a gift rather than a burden. Since the server keeps no context between requests, any request can be handled by any instance of the service — you can spin up ten copies behind a balancer, and they're interchangeable. State (sessions, carts) is moved into shared storage, while the servers themselves stay identical and scale easily.
Where this applies
HTTP is the foundation almost the entire backend stands on: REST APIs, webhooks, calls between microservices. Mastering it means understanding not just how to send a request, but how to respond correctly.
- Return the right status codes. Created a resource —
201, not200. Didn't find it —404, not200with an empty body. Invalid input —400; no rights —403. Clients (and monitoring) make decisions off the status code, so "always200, error in the body" breaks all the automation around it. - Split
4xxand5xxhonestly. A validation error is a4xx(the request is at fault), not a500. Returning500for bad input makes metrics show a false outage, and the client starts pointlessly retrying something a repeat won't fix. - Keep idempotency under retries. Operations a client can safely repeat (replace, delete) should be designed idempotent. For non-idempotent creating operations, build in an idempotency key so that a lost response and a repeated request don't create a duplicate — a payment or an order twice.
Where beginners stumble:
- They return
200for everything, even errors. Then the client is forced to parse the body to tell success from failure — while the whole point of status codes is to make that visible at a glance. - They confuse
401and403.401— I don't know who you are (authentication needed);403— I know, but you're not allowed (no rights). Different problems, different fixes. - They treat
GETas a place for changes. "I'll just do aGET /delete?id=5" — and one day a search crawler or a browser prefetcher walks the links and wipes the data.GETmust be safe. - They forget HTTP is stateless. They expect the server to "remember" the previous request and don't send authentication again — but it remembers nothing.
What to learn next
HTTP doesn't live in a vacuum. Underneath it, the transport carries it — how a request even reaches the server is covered in the article on the OSI and TCP/IP models. On top of HTTP goes encryption: the same protocol, but over a secure channel — that's HTTPS and TLS. The protocol itself has evolved too — how HTTP/2 and HTTP/3 differ from the old HTTP/1.1 is in the article on HTTP versions. And when something in this exchange breaks — timeouts, retries, 5xx codes — the techniques from the article on reliability come to the rescue.
The next big step is turning these methods and status codes into a convenient, predictable service interface. That's API design: the continuation is in the section on REST API.