Step 01 of 24
How backends work
Trace an HTTP request from client to server and back, and pick the right method and status code for every response.
Every app you use talks to a backend: a program on a server that receives requests, does work (reads a database, runs business rules) and sends back responses. Before you write any Go, you need a precise model of that conversation. This lesson covers the shape of an HTTP exchange, what methods and status codes mean, why backends are stateless, and what happens to a request on its way to your code.
What a backend actually does
A backend is a long-running process that listens on a network port. Unlike a script, it does not run top to bottom and exit: it binds to a port such as 8080 and waits. Each time a request arrives it runs a small piece of code (a handler), writes a response, and goes back to waiting.
Most backends you will build follow the same loop for every request:
- Parse the request: which resource, which action, what data.
- Authenticate and authorize: who is asking, and may they?
- Validate the input: well-formed and within limits?
- Do the work: query or update a database, call other services.
- Respond with a status code, headers and usually a JSON body.
The client never sees your code or your database, only the response. The HTTP layer is your contract with every client, so its details matter as much as the business logic.
Anatomy of an HTTP exchange
HTTP is a request–response protocol. The client sends one request; the server sends exactly one response. In HTTP/1.1 both are plain text with a fixed layout, which you can see with curl -i (show response headers) or curl -v (show everything).
Here is a request to create a task:
POST /tasks HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
Content-Length: 26
{"title":"Write the docs"}It has three parts:
- Request line: the method (
POST), the target path (/tasks, optionally with a query string like?done=false), and the protocol version. - Headers:
Name: valuemetadata.Hostnames the site (one server can host many),Content-Typedescribes the body,Acceptsays what format the client wants back. - Body: optional bytes, separated from the headers by a blank line.
GETrequests normally have no body.
The server answers:
HTTP/1.1 201 Created
Content-Type: application/json
Location: /tasks/42
Content-Length: 47
{"id":42,"title":"Write the docs","done":false}The status line carries a three-digit code and a human-readable reason. Headers follow, then the body. Location points at the newly created resource, which is the conventional companion to 201 Created.
HTTP/2 and HTTP/3 encode the same pieces as binary frames, but the model is identical: method, path, headers, body in; status, headers, body out. Go's server handles HTTP/1.1 for you, and HTTP/2 automatically over HTTPS.
Methods and their guarantees
The method tells the server what the client wants to do with the resource named by the path. The important part is not the name but the guarantees each method makes, because clients, proxies and browsers rely on them.
| Method | Typical use | Safe | Idempotent |
|---|---|---|---|
GET | Read a resource | Yes | Yes |
HEAD | Like GET, headers only | Yes | Yes |
POST | Create, or trigger an action | No | No |
PUT | Replace a resource entirely | No | Yes |
PATCH | Change part of a resource | No | Not guaranteed |
DELETE | Remove a resource | No | Yes |
- Safe means the request does not change server state. A crawler or a browser prefetch can issue it freely. Never let a
GET /tasks/42/deletelink delete anything. - Idempotent means sending the request once or ten times leaves the server in the same state.
PUT /tasks/42with the same body always produces the same task;DELETE /tasks/42leaves it gone whether or not it was already gone.POST /taskstwice creates two tasks.
Idempotency is what makes retries safe. Networks drop responses: the server did the work but the reply never arrived. An idempotent request can simply be retried; retrying a POST might charge a card twice, which is why payment APIs accept an idempotency key that lets the server recognise a repeat.
Status codes
The status code is the first thing a client checks, so it must be accurate on its own. Codes are grouped by their first digit:
| Class | Meaning | Who is "at fault" |
|---|---|---|
1xx | Informational, rarely used directly | — |
2xx | Success | — |
3xx | Redirection: look elsewhere | — |
4xx | Client error: the request is wrong | Client |
5xx | Server error: the request was fine, the server failed | Server |
The codes you will use every week:
| Code | When to use it |
|---|---|
200 OK | Success with a body (reads, updates that return the resource) |
201 Created | A new resource was created; add a Location header |
204 No Content | Success with no body, common for DELETE |
400 Bad Request | Malformed input: invalid JSON, a non-numeric ID |
401 Unauthorized | No valid credentials: "who are you?" |
403 Forbidden | Valid credentials, not permitted: "I know you, and no" |
404 Not Found | No such resource |
405 Method Not Allowed | The path exists but not with this method; include Allow |
409 Conflict | The request clashes with current state, e.g. a duplicate email |
422 Unprocessable Content | Well-formed but semantically invalid, e.g. a negative quantity |
429 Too Many Requests | Rate limit hit; often with Retry-After |
500 Internal Server Error | An unexpected failure in your code |
502 Bad Gateway | A proxy got an invalid response from the server behind it |
503 Service Unavailable | Temporarily overloaded or down for maintenance |
504 Gateway Timeout | A proxy gave up waiting for the server behind it |
The 4xx/5xx split matters operationally. A 4xx says "fix the request before retrying". A 5xx says "we broke", should page someone if it spikes, and may be retried. Returning 500 for bad input fills your alerts with noise; returning 400 for a crashed database hides an outage.
Statelessness: where state lives
HTTP is stateless: each request must carry everything the server needs to handle it. The server does not remember that the same client sent a request a moment ago. If the client is logged in, every request carries proof, such as a session cookie or a token in the Authorization header.
Your backend should follow the same rule internally: do not keep per-client state in one process's memory. In production you run several copies of your service behind a load balancer:
┌──────────────┐
┌───►│ instance A │──┐
client ─► load ───┤ └──────────────┘ ├──► PostgreSQL
balancer │ ┌──────────────┐ │
└───►│ instance B │──┘
└──────────────┘If instance A stores a user's shopping cart in a Go map, the user's next request may land on instance B, which has never heard of the cart, and when A restarts during a deploy the cart is gone. Keep state in a shared store (a database or a cache like Redis) and treat instances as interchangeable. Then you can add instances for more traffic and restart any of them without losing anything.
In-memory data is fine when it is safe to lose and identical everywhere, such as compiled templates or a connection pool.
The journey of a request
When a client calls https://api.example.com/tasks, a lot happens before your handler runs:
- DNS: the client resolves
api.example.comto an IP address. - TCP and TLS: it opens a connection and performs a TLS handshake so traffic is encrypted. The connection is kept alive and reused for later requests.
- Load balancer or reverse proxy (for example nginx, Caddy or a cloud load balancer): often terminates TLS, then forwards the request to one healthy instance of your service.
- Your server: Go's
net/httpparses the request and calls your handler, each request in its own goroutine. - Downstream calls: your handler queries the database or other services, then writes the response, which travels back the same way.
Here is the POST /tasks exchange from earlier, hop by hop, once DNS has resolved and the connection is open. Step through it and notice that the load balancer is the client's only contact, and that the response retraces the request's path.
Every hop can fail or be slow, and every hop has a timeout. This explains the gateway codes: if your process crashes mid-response, the proxy returns 502; if your handler outlasts the proxy's timeout, the client gets 504 while your code may still be running. Later in this roadmap you will set your own server timeouts so slow clients and handlers cannot exhaust your service.
Practice
You are designing the API for a small library system. For each interaction, write the request line (method and path) and the status code the server should return. Assume books live under /books and each has a numeric ID.
- A user lists all books.
- A librarian adds a new book; the server assigns ID
17. - Someone fetches book
999, which does not exist. - A client sends
{"title":(cut off mid-JSON) when adding a book. - A user who is not logged in tries to delete book
17. - A logged-in user who is not a librarian tries to delete book
17. - A librarian deletes book
17. - A client sends
PATCH /books(no ID); onlyGETandPOSTexist on that path.
Show solution
| # | Request line | Status |
|---|---|---|
| 1 | GET /books | 200 OK with the list |
| 2 | POST /books | 201 Created, Location: /books/17 |
| 3 | GET /books/999 | 404 Not Found |
| 4 | POST /books | 400 Bad Request: the body is not valid JSON |
| 5 | DELETE /books/17 | 401 Unauthorized: no credentials |
| 6 | DELETE /books/17 | 403 Forbidden: authenticated, not permitted |
| 7 | DELETE /books/17 | 204 No Content |
| 8 | PATCH /books | 405 Method Not Allowed, Allow: GET, HEAD, POST |
In case 4 the problem is syntax, so 400 fits; valid JSON with an empty title would be 422. In case 8 Allow lists HEAD because supporting GET implies HEAD, and Go's router adds it automatically.
Key takeaways
- A backend is a long-running process that turns each request (method, path, headers, body) into exactly one response (status, headers, body).
- Safe methods do not change state; idempotent methods can be repeated without extra effect, which is what makes retries safe.
POSTis neither. - The status code must be truthful on its own:
4xxblames the client,5xxblames the server, and200with an error body is always wrong. 401means "not authenticated",403means "authenticated but not allowed".- Keep instances stateless and put shared state in a database or cache, so any instance can serve any request and can be restarted freely.
- Requests pass through DNS, TLS, proxies and load balancers before reaching your code, and each hop has its own timeouts and failure codes.
Check yourself
Quiz
0/4 correct · 4 left
1.A mobile client sends PUT /profile, the server updates the profile, but the response is lost and the client retries the identical request. Which property makes this retry harmless?
2.A user is logged in with a valid token but tries to delete a post that belongs to someone else. Which status code fits best?
3.Why should a Go service running as several instances behind a load balancer avoid storing users' shopping carts in an in-process map?
4.Your handler takes 90 seconds to finish, but the reverse proxy in front of it waits at most 60 seconds. What does the client most likely receive?
Finished reading?
Mark it complete to fill in your roadmap.