# REST API 101: read a resource, change it deliberately

An original 45-minute lesson for engineers who know functions and records but
are new to web API design. Define the pieces, follow the diagram, predict seven
exchanges, then inspect actual HTTP and state changes. All tickets are fictional.

## 1. Give each piece a different job

An **API** is a software interface: the operations, inputs, outputs and errors
that a caller can rely on. **REST**, Representational State Transfer, is an
architectural style for networked applications. It organizes interactions around
resources and a uniform interface. It is not a programming language, product or
synonym for returning JSON.

Think of a ticket as a record behind a service. `/tickets/T-104` identifies it.
The JSON you receive is a **representation** of that resource. It need not expose
every internal database field. **HTTP** carries the request and response;
**JSON** is the data format chosen here. **FastAPI** is one Python framework for
implementing such a service. This lab uses Python's standard library so you can
inspect the exchange without installing a framework.

| Piece | In our fictional service | Familiar comparison |
| --- | --- | --- |
| Resource | Ticket T-104 | A record with an identity |
| Address | `/tickets/T-104` | The reference used to find the record |
| Representation | `{"title":"Pump check","status":"open"}` | A public view of the record |
| Method | GET, POST, PUT or DELETE | The requested operation |
| Response | Status, headers and sometimes a body | A result with explicit metadata |

The function-call analogy has a limit: a remote call can fail after the service
has performed work. A caller must reason about what it observed, not assume
that a missing response means nothing happened. An FDE designing an agent tool
needs to distinguish reading a ticket from creating or replacing one before
giving a model access to that tool.

REST also includes client/server separation, stateless requests, cache rules,
layering and a uniform interface, including hypermedia controls; code on demand
is optional. This small lab teaches resource interactions and HTTP semantics,
not every REST constraint. Stateless requests do not forbid a server from
storing tickets. The server still needs authentication and authorization in a
real system; a method, identifier or version tag supplies none of those.

## 2. Read the method before repeating the call

| Method | Our contract | What to predict on repetition |
| --- | --- | --- |
| GET `/tickets/T-104` | Read the current representation | No requested business mutation |
| POST `/tickets` | Create a new ticket; return its address in `Location` | Two submissions can create two tickets |
| PUT `/tickets/T-104` | Replace the complete writable representation, if its version still matches | Same intended effect; a repeated condition may now fail |
| DELETE `/tickets/T-104` | Remove this resource | It stays absent; response statuses can differ |

**Safe** describes a method whose requested semantics are read-only. Logging a
GET is possible; silently dispatching a technician on GET violates the read
contract. **Idempotent** means identical requests have the same intended effect
as one request. It does not promise identical responses or that replaying an old
intent is always appropriate after another person's edit. GET, PUT and DELETE
are idempotent by HTTP semantics; POST has no general replay guarantee.

The lab's PUT replaces `title` and `status`. Sending just `status` is refused
instead of silently treating PUT as a partial update. PATCH is another HTTP
method for a defined patch document; it is not implemented here, and whether a
particular patch is idempotent depends on its operation.

An **ETag**, or entity tag, identifies a representation version chosen by the
service. It is an opaque value: copy it exactly rather than deriving one from
an integer you guessed. The **If-Match** request header says to perform this
change only if the current representation matches the supplied tag. Our narrow
contract accepts one strong tag returned by GET. It does not implement tag
lists, wildcards, cache validation or the rest of HTTP's conditional syntax.

## 3. Predict and run seven exchanges

Download `fde-rest-resource-lab.py` into your practice folder. Python 3.11+ is
enough; there are no packages, accounts or credentials to configure. The program
starts a short-lived service on a randomly assigned **127.0.0.1** port, makes
real local HTTP requests, and closes it. It never binds to a public interface,
contacts an external service or writes ticket data to disk. Use fictional data.
Python's teaching HTTP server is not a production deployment template.

```sh
python3 --version
python3 fde-rest-resource-lab.py compare
python3 fde-rest-resource-lab.py stale-edit
```

Before running, write the statuses and final ticket state you expect. Each case
starts from its own T-104 with title `Pump check`, status `open` and ETag
`"T-104-v1"`. Each trace records the request, response and a teaching-only
inspection of server state. A real client normally cannot inspect that last
field; it needs the service's documented read or reconciliation interface.

| Case | Expected status sequence | State to inspect |
| --- | --- | --- |
| `read` | 200, 200 | Same one ticket and same ETag |
| `create-repeat` | 201, 201 | New T-105 and T-106, each with its own Location |
| `replace-repeat` | 200, 200, 412 | GET, then two identical conditional PUTs; T-104 stays queued at v2 |
| `delete-repeat` | 204, 404 | T-104 is absent after both DELETEs |
| `stale-edit` | 200, 200, 412, 200 | Another editor's title and queued status survive the stale PUT |
| `missing-precondition` | 428 | No change; the required If-Match header was absent |
| `partial-replacement` | 200, 400 | No change; the complete writable representation was not supplied |

200 means this request succeeded. 201 identifies creation; inspect Location.
204 has no response body. 404 says this resource was not found in this lab.
400 rejects our malformed representation. 412 means the condition did not hold.
428 means this service requires a condition; not all services enforce that rule.
Read the specific contract rather than assuming every error is retryable.

## 4. Follow the edit that arrived too late

The `stale-edit` case models two callers in a deliberate sequential order. It is
not a concurrent-load test:

1. Editor A reads T-104 at v1 and prepares to close it with the original title.
2. Editor B replaces it with `Pump check: reviewed scope`, status `queued`, using
   v1 in If-Match. The response is 200 and the new representation is v2.
3. Editor A sends the older full representation with v1. The server compares
   the tag before mutation, returns 412 and keeps B's representation.
4. A fresh GET shows B's title and queued status. A must compare the changes and
   decide what is still intended before sending another update.

Read `dispatch` in the program. Find the validation, missing-condition and
mismatched-condition branches before `row["body"] = raw`. The exact submitted
JSON bytes are stored and returned; the ETag sits in a response header, not in a
field the caller can overwrite. There is no login or permission check in this
fictional local service. Version checking and authorization are different jobs.

In `replace-repeat`, the first PUT changes v1 to v2. The second identical request
still carries v1, so it receives 412 while the resource remains queued. Different
statuses do not disprove idempotency. They also do not prove who performed an
earlier change. After uncertain delivery, read/reconcile according to the
service contract instead of declaring the operation failed or blindly resending
with a new tag. This lab does not simulate a network timeout or implement
transactional idempotency keys.

## 5. Change one cause at a time

Make two separate copies. Keep the reference file unchanged.

**Experiment A — count creations.** In one copy, change `CREATE_ATTEMPTS = 2`
to `CREATE_ATTEMPTS = 1`. Run `create-repeat`. There should be one 201, one
Location and just one new ticket. The original two creations were actual server
mutations, not repeated display of the same result. A caller-generated key
would need an explicit server-side deduplication contract; merely adding an
undocumented header cannot establish one.

**Experiment B — expose a misleading repair.** In the other copy, set
`REFRESH_BEFORE_SECOND_EDIT = True` and run `stale-edit`. The code gets a fresh
ETag but resends A's old body without reconciling it. Expect statuses
200, 200, 200, 200, 200 and final title `Pump check`, status `closed`, v3.
B's reviewed title was overwritten. This is a deliberately flawed client
strategy in the local exercise, not the recommended repair.

Explain why the server's condition passed. It verifies that the submitted tag
matches now; it cannot tell whether A actually reviewed the current content.
Write a better client sequence: read current content, compare with the original,
reconcile the intended change, then submit the reviewed representation with the
matching tag. A new 412 should restart that review instead of forcing a write.

## 6. Turn the API into an agent tool contract

An agent proposes closing T-104 after reading v1. While it is thinking, a human
changes the scope. Sketch the components: model proposal → application policy →
HTTP client → resource service → observed result. State who owns each check.

Produce a short contract with:

- A read-only tool separate from the write tool, with explicit allowed fields.
- Trusted caller and resource authorization outside model-generated arguments.
- A version tied to the representation that was actually reviewed.
- A 412 path that returns for reconciliation, a 428 configuration repair and an
  uncertain-delivery path that preserves an unknown outcome.
- A distinction between receiving a 2xx response and completing the customer's
  intended work. Changing a ticket record does not prove a technician was sent.

Discuss one new deployment boundary: TLS, identity, durable storage, atomic
compare-and-update, auditing, request limits or crash recovery. The sequential
in-memory lab has not established these. Choose the smallest test that would
show the boundary you are proposing, using an isolated environment.

## 7. Keep the evidence and go deeper

Keep seven predictions and observed traces, both controlled edits, your repaired
client sequence and the agent tool contract. A useful explanation names the
resource, representation, operation, observed state and remaining uncertainty.

Continue with [the first FastAPI request](/training/guides/agent-engineering#0d-your-first-http-request-with-fastapi)
to see a framework route and validate the input. Use [networking 101](/training/guides/networking)
for connection boundaries, and [action recovery](/training/guides/action-recovery)
for the harder case where a response is lost after an external effect.

Primary references checked 21 October 2026:

- [MDN: REST](https://developer.mozilla.org/en-US/docs/Glossary/REST)
- [IETF: HTTP semantics, methods and conditions](https://www.rfc-editor.org/rfc/rfc9110.html)
- [MDN: If-Match and representation comparison](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-Match)
- [IETF: 428 Precondition Required](https://www.rfc-editor.org/rfc/rfc6585.html#section-3)
- [Python: the standard-library HTTP server and its limits](https://docs.python.org/3.11/library/http.server.html)
