Getting started¶
This walks you through running the service and placing an order through it. It takes about five minutes.
The service is a complete, running microservice.
What you need¶
| Tool | Why |
|---|---|
| uv | The only Python tool required. It installs Python itself, resolves dependencies, and runs commands. |
| just | A command runner. Every task in this project is a just recipe. |
| Docker | Only for just up, in Start with data in it. Nothing else below needs it. |
You do not need to install Python separately. uv reads
.python-version and fetches the right interpreter.
Run it¶
uv sync
uv sync installs exactly the versions recorded in uv.lock, so you get the
same dependency tree the tests and the container image use.
Install the git hooks once. They check formatting and commit messages before a commit is created:
uv run pre-commit install
Now start the service:
just dev
That serves on http://localhost:8000 with auto-reload: edit a file and the server restarts itself. Interactive API documentation is at http://localhost:8000/docs.
Start with data in it¶
just dev starts empty. With Docker, the container stack starts with five
orders already in it. Stop just dev first (Ctrl-C) — both serve on port
8000 — then:
just up
That builds the image, starts PostgreSQL, Redis, MinIO and a payment stub,
applies the migrations, and starts the API. Once the API reports healthy, a
one-shot seed container creates five fixed orders through the HTTP API and
exits. The line to look for in the output is:
seed-1 exited with code 0
The orders it created, with the ids the service assigned them:
docker compose logs seed
seed-1 | espresso-beans c8e7aa28-be07-4916-9b1e-eb2cae1f654a 37.00 EUR created
seed-1 | filter-papers 603394d1-950d-4c6f-a388-d4c8eee60f0e 12.75 EUR created
seed-1 | grinder eba6a954-cee1-45c5-ba74-3804a4d55af6 249.00 EUR created
seed-1 | mixed-basket 6d580aeb-2dbd-4240-9bfd-20a890d324f1 52.00 EUR created
seed-1 | bulk-order ac7eefb7-d04c-4fdc-8af5-34afffcb6f78 462.50 USD created
Any of those ids answers a GET straight away, before you have placed
anything yourself:
curl -s http://localhost:8000/api/v1/orders/c8e7aa28-be07-4916-9b1e-eb2cae1f654a | jq
The ids differ on every machine — the service assigns them — so copy one
from your own seed output rather than from this page.
The seed is idempotent (running it again changes nothing): the ids it issued
are kept in a small state file, and a second just up finds every order still
there and prints exists instead of created. just down removes the state
with the database, so the next just up seeds again.
The same five orders are available without Docker. With just dev running in
one terminal, in another:
just seed
That runs the same module against localhost:8000, keeps its state in
.seed-state.json (ignored by git), and prints the same five lines. just dev
holds orders in memory, so after a restart just seed finds them gone and
creates them again.
Check that it is alive¶
response=$(curl -si http://localhost:8000/healthz)
echo "$response"
grep -q '^HTTP/1.1 200' <<< "$response"
grep -q '"status":"ok"' <<< "$response"
HTTP/1.1 200 OK
content-type: application/json
x-request-id: 3d4cd25c9d334f65b6fb555d12dac32e
{"status":"ok","version":"0.1.0"}
Note x-request-id. Every response carries one. It is the correlation
identifier — a single value that ties together every log line one request
produced. You did not send one, so the service generated it. Send your own
and the service uses that instead, which is how you follow one request across
several services.
There are three health endpoints, not one, and they answer three different questions. HTTP API explains why that distinction prevents an outage.
Place an order¶
response=$(curl -si -X POST http://localhost:8000/api/v1/orders -H 'Content-Type: application/json' -d '{"customer_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6","lines":[{"sku":"WIDGET-1","quantity":2,"unit_amount":"9.99","currency":"EUR"}]}')
echo "$response"
grep -q '^HTTP/1.1 201' <<< "$response"
grep -q '"amount":"19.98"' <<< "$response"
HTTP/1.1 201 Created
content-type: application/json
location: /api/v1/orders/cac0acf8-ea5c-4936-97b4-3b0c113f8a8f
x-request-id: f69a9e6446c34a8387327ebf80482709
{
"id": "cac0acf8-ea5c-4936-97b4-3b0c113f8a8f",
"customer_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"lines": [
{
"sku": "WIDGET-1",
"quantity": 2,
"unit_price": {"amount": "9.99", "currency": "EUR"},
"subtotal": {"amount": "19.98", "currency": "EUR"}
}
],
"total": {"amount": "19.98", "currency": "EUR"}
}
Three things happened that are worth naming.
subtotal and total were computed, not accepted. You sent a unit price
and a quantity. The Order entity works out the rest and refuses to exist if
the total disagrees with its lines.
Money is a JSON string, not a number. "19.98", not 19.98. Amounts are
Decimal values, and rendering one as a JSON number would route it through a
binary floating-point type, where 0.1 + 0.2 is not 0.3. A string crosses
the wire exactly.
The response is not the stored entity. The stored Order also has an
internal_note field. It is absent above, and it cannot leak by accident,
because the response is built by a mapper function that lists what goes out.
That is why API schemas are separate.
Ask for one that does not exist¶
response=$(curl -si http://localhost:8000/api/v1/orders/3fa85f64-5717-4562-b3fc-2c963f66afa6)
echo "$response"
grep -q '^HTTP/1.1 404' <<< "$response"
grep -q '^content-type: application/problem+json' <<< "$response"
HTTP/1.1 404 Not Found
content-type: application/problem+json
{
"type": "https://errors.example.com/order_not_found",
"title": "Order not found",
"status": 404,
"detail": "no order with id 3fa85f64-5717-4562-b3fc-2c963f66afa6",
"instance": "/api/v1/orders/3fa85f64-5717-4562-b3fc-2c963f66afa6"
}
The content type is application/problem+json, not application/json. This
is RFC 9457 Problem Details — the internet standard
shape for an error body, so a client can read errors from any service the
same way.
The domain layer never chose 404. It raised OrderNotFoundError, which
says which business rule broke. One module in the api layer decides that
"not found" means 404. That separation is the dependency
rule at work.
Orders do not survive a restart of just dev
With no database configured, the service stores orders in memory, on
purpose. Restart just dev and the order is gone — just seed puts the
five fixed ones back. just up runs PostgreSQL behind the same
interface, and nothing above the storage layer changes when it does.
Run the checks¶
One command runs everything a pull request must pass:
just check
That is lint, type-check, the import rule, the tests, and the git hooks — then
git diff --exit-code. The last part matters: several hooks rewrite files,
so a run that reformatted your code but exited 0 would look like a pass. The
diff check turns that into a loud failure. Every command is listed in
Commands.
Where to go next¶
- Add an endpoint — build your own feature through all four layers.
- Architecture — how the pieces fit and why.
- Configuration — every environment variable.