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.

13 min 8 sections

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:

  1. Parse the request: which resource, which action, what data.
  2. Authenticate and authorize: who is asking, and may they?
  3. Validate the input: well-formed and within limits?
  4. Do the work: query or update a database, call other services.
  5. 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:

text
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: value metadata. Host names the site (one server can host many), Content-Type describes the body, Accept says what format the client wants back.
  • Body: optional bytes, separated from the headers by a blank line. GET requests normally have no body.

The server answers:

text
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.

MethodTypical useSafeIdempotent
GETRead a resourceYesYes
HEADLike GET, headers onlyYesYes
POSTCreate, or trigger an actionNoNo
PUTReplace a resource entirelyNoYes
PATCHChange part of a resourceNoNot guaranteed
DELETERemove a resourceNoYes
  • 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/delete link delete anything.
  • Idempotent means sending the request once or ten times leaves the server in the same state. PUT /tasks/42 with the same body always produces the same task; DELETE /tasks/42 leaves it gone whether or not it was already gone. POST /tasks twice 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:

ClassMeaningWho is "at fault"
1xxInformational, rarely used directly—
2xxSuccess—
3xxRedirection: look elsewhere—
4xxClient error: the request is wrongClient
5xxServer error: the request was fine, the server failedServer

The codes you will use every week:

CodeWhen to use it
200 OKSuccess with a body (reads, updates that return the resource)
201 CreatedA new resource was created; add a Location header
204 No ContentSuccess with no body, common for DELETE
400 Bad RequestMalformed input: invalid JSON, a non-numeric ID
401 UnauthorizedNo valid credentials: "who are you?"
403 ForbiddenValid credentials, not permitted: "I know you, and no"
404 Not FoundNo such resource
405 Method Not AllowedThe path exists but not with this method; include Allow
409 ConflictThe request clashes with current state, e.g. a duplicate email
422 Unprocessable ContentWell-formed but semantically invalid, e.g. a negative quantity
429 Too Many RequestsRate limit hit; often with Retry-After
500 Internal Server ErrorAn unexpected failure in your code
502 Bad GatewayA proxy got an invalid response from the server behind it
503 Service UnavailableTemporarily overloaded or down for maintenance
504 Gateway TimeoutA 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:

text
                       ┌──────────────┐
                  ┌───►│  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:

  1. DNS: the client resolves api.example.com to an IP address.
  2. 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.
  3. 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.
  4. Your server: Go's net/http parses the request and calls your handler, each request in its own goroutine.
  5. 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.

DiagramPOST /tasks through a load balancer
ClientLoad balancerGo serverDatabase1POST /tasks2POST /tasks3Run handler4INSERT task5id = 426201 Created7201 Created

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.

  1. A user lists all books.
  2. A librarian adds a new book; the server assigns ID 17.
  3. Someone fetches book 999, which does not exist.
  4. A client sends {"title": (cut off mid-JSON) when adding a book.
  5. A user who is not logged in tries to delete book 17.
  6. A logged-in user who is not a librarian tries to delete book 17.
  7. A librarian deletes book 17.
  8. A client sends PATCH /books (no ID); only GET and POST exist on that path.
Show solution
#Request lineStatus
1GET /books200 OK with the list
2POST /books201 Created, Location: /books/17
3GET /books/999404 Not Found
4POST /books400 Bad Request: the body is not valid JSON
5DELETE /books/17401 Unauthorized: no credentials
6DELETE /books/17403 Forbidden: authenticated, not permitted
7DELETE /books/17204 No Content
8PATCH /books405 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. POST is neither.
  • The status code must be truthful on its own: 4xx blames the client, 5xx blames the server, and 200 with an error body is always wrong.
  • 401 means "not authenticated", 403 means "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. 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. 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. 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. 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.