# How backends work

> Trace an HTTP request from client to server and back, and pick the right method and status code for every response.

Backend with Go roadmap · Foundations · 13 min read · https://levelcode.org/roadmaps/go-backend/how-backends-work

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.

> [!NOTE]
> Header names are case-insensitive: `content-type` and `Content-Type` are the same header. HTTP/2 sends them in lowercase on the wire. Never write code that depends on the capitalisation a client used.

## 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/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:

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

> [!WARNING]
> Never return `200 OK` with a body like `{"error": "not found"}`. Monitoring, retries, caches and client libraries all key off the status code. A wrong code silently breaks every one of them.

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.

```sequence
title: POST /tasks through a load balancer
participants: Client, Load balancer, Go server, Database
Client -> Load balancer: POST /tasks
> The client only ever talks to the load balancer's address. The load balancer usually terminates TLS, so it can read the request and route it.
Load balancer -> Go server: POST /tasks
> It picks one healthy instance and forwards the request, adding headers such as X-Forwarded-For with the client's real IP.
Go server -> Go server: Run handler
> net/http parses the request and runs your handler in its own goroutine. The handler authenticates and validates first, so bad input gets a 400 before the database is touched.
Go server -> Database: INSERT task
> Something like INSERT INTO tasks ... RETURNING id, sent on a connection from the pool, so there is no connection setup per request.
Database --> Go server: id = 42
Go server --> Load balancer: 201 Created
> The handler writes the status, headers (including Location: /tasks/42) and JSON body. If it took longer than the load balancer's timeout, the client would already have received a 504.
Load balancer --> Client: 201 Created
> The load balancer relays the response, encrypted, on the client's connection, which stays open for the next request.
```

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.

> [!TIP]
> Behind a proxy, the client's real IP is not the TCP peer address; it arrives in a header such as `X-Forwarded-For`. Only trust that header when it was set by your own proxy, since clients can send it too.

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

**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. `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.

## Quiz

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?

   A. PUT is a safe method
   B. PUT is idempotent
   C. HTTP is stateless
   D. PUT requests cannot have a body

   **Answer: B.** Idempotent means repeating the request leaves the server in the same state as sending it once. PUT is not safe (it changes state), and statelessness is about requests carrying their own context, not about repeat effects.

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?

   A. 400 Bad Request
   B. 401 Unauthorized
   C. 404 Not Found
   D. 403 Forbidden

   **Answer: D.** 403 means the server knows who you are and you are not allowed to do this. 401 is for missing or invalid credentials, which is not the case here. (Some APIs deliberately return 404 to hide that the resource exists, but 403 is the accurate code.)

3. Why should a Go service running as several instances behind a load balancer avoid storing users' shopping carts in an in-process map?

   A. The next request may reach a different instance, and the data is lost when the process restarts
   B. Go maps cannot store more than a few thousand entries
   C. HTTP forbids servers from remembering anything between requests
   D. The load balancer will reject requests to instances that use memory

   **Answer: A.** Each instance has its own memory, so in-process state is invisible to the other instances and disappears on restart. HTTP's statelessness is about requests, not a ban on servers storing data: shared stores such as a database are the right place for it.

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?

   A. 500 Internal Server Error
   B. 503 Service Unavailable
   C. 504 Gateway Timeout
   D. 200 OK after 90 seconds

   **Answer: C.** The proxy gives up waiting for the upstream server and returns 504 Gateway Timeout. 502 would mean the upstream sent an invalid response, and 500 comes from your own code, which never got to answer the client.
