# Worked Example: Job-Application Tracker (Puls-Backend)

Session: 2026-08-13, user Chris Bringemeier. Goal: HTTP backend on the host
that tracks his Bewerbungen with status, so a future agent/frontend can
read & write over HTTP.

## What was built

FastAPI + SQLAlchemy + SQLite, served from `~/puls-backend/` on
`http://127.0.0.1:8000`. Domain model:

```python
class Application(Base):
    id, company, position, location, source, status,
    applied_at (date), last_update (datetime, onupdate=utcnow),
    contact_name, contact_email, job_id, notes
```

Status enum: `open | interview | offer | rejected | withdrawn` — enforced
via `Literal[...]` in Pydantic, gives free 422 on bad values.

## Routes

```
GET    /                       banner
GET    /health                 {"status":"ok"}
GET    /applications           list, ?status= & ?q= filter
GET    /applications/{id}      404 on miss
POST   /applications           201, 422 on validation
PUT    /applications/{id}      partial update via exclude_unset
DELETE /applications/{id}      204
```

## Server startup (the pattern that works)

```bash
cd ~/puls-backend
uv sync                          # creates .venv, installs deps
# background terminal, no notify:
uv run uvicorn app.main:app --host 127.0.0.1 --port 8000
```

## Verification log (actual outputs from this session)

- `GET /health` → `{"status":"ok"}` ✓
- `GET /applications` (empty) → `[]` ✓
- `POST /applications` × 8 rows (seeded from real inbox data) → all 201
- `GET /applications?status=open` → 2 hits (Produkt+Markt, Stepstone Rails)
- `GET /applications?q=rails` → 1 hit (Stepstone Rails Backend)
- `PUT /applications/1` (SPIE → interview → rejected, with notes round-trip) → ok
- `POST /applications` with `{"company": "Test"}` (missing `position`) → **422** ✓
- `GET /applications/9999` → **404** ✓

## Seeding pattern

User's data was already in email. Each row was POSTed individually via
`urllib.request` from an `execute_code` block — verified each write by
inspecting the response. This is the move: don't bulk-insert via raw SQL
when you can verify the API contract by exercising it.

```python
import json, urllib.request
for app in seed_rows:
    req = urllib.request.Request(
        "http://127.0.0.1:8000/applications",
        data=json.dumps(app).encode(),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(req) as r:
        created.append(json.loads(r.read()))
```

## Outcome

8 Bewerbungen in DB, all CRUD paths verified live, server still running in
background. Next session can hit the API immediately.