A FastAPI service that manages Medication Requests for clinical research staff, associating existing Patients, Clinicians, and Medications.
- Python 3.10+ / FastAPI / Pydantic v2
- SQLAlchemy 2.0 with Alembic migrations
- PostgreSQL (runtime), SQLite in-memory (test suite)
- pytest, mypy (strict), Docker + docker-compose
docker compose up --buildThis starts PostgreSQL, applies migrations, seeds sample data, and serves the API at http://localhost:8000. Interactive OpenAPI docs: http://localhost:8000/docs
Requires a running PostgreSQL instance (or use docker compose up db).
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements-dev.txt
cp .env.example .env # adjust DATABASE_URL if needed
alembic upgrade head # apply migrations
python -m app.seed # insert sample data
uvicorn app.main:app --reloadTests run against an in-memory SQLite database — no PostgreSQL needed:
pip install -r requirements-dev.txt
pytestStatic type checking:
mypy app| Method | Path | Description |
|---|---|---|
| POST | /patients/{patientId}/medication-requests |
Create a medication request |
| GET | /patients/{patientId}/medication-requests |
List requests; filters: status, prescribed_from, prescribed_to |
| PATCH | /medication-requests/{id} |
Update end_date, frequency, status only |
The seed data uses fixed UUIDs so these work out of the box:
curl -X POST http://localhost:8000/patients/11111111-1111-1111-1111-111111111111/medication-requests \
-H "Content-Type: application/json" \
-d '{
"clinician_id": "22222222-2222-2222-2222-222222222222",
"medication_id": "33333333-3333-3333-3333-333333333333",
"reason_text": "Schistosomiasis treatment",
"prescribed_date": "2026-07-01",
"start_date": "2026-07-02",
"end_date": "2026-07-30",
"frequency": "3 times/day"
}'curl "http://localhost:8000/patients/11111111-1111-1111-1111-111111111111/medication-requests?status=active&prescribed_from=2026-07-01"- No overlapping active requests — a patient may not have more than one
active request for the same medication whose effective dates
(
start_date..end_date, open-ended ifend_dateis null) overlap. Violations return409 Conflict. Enforced on create and on PATCH (including re-activating an on-hold request or extending an end date). - No resurrection — a
completedorcancelledrequest may not transition back toactive(409 Conflict). - Date ordering —
end_datecannot be earlier thanstart_date(422on create and on PATCH). - Restricted PATCH — only
end_date,frequency, andstatusare accepted; any other field is rejected with422(Pydanticextra="forbid").
- Service layer (app/services/medication_requests.py)
holds all business rules, keeping routers thin and rules unit-testable
independently of HTTP. Domain exceptions (
NotFoundError,ConflictError,BusinessRuleError) are mapped to HTTP status codes by exception handlers in app/main.py, so the service layer stays framework-agnostic. - 404 for missing references — creating a request with a non-existent
patient, clinician, or medication returns
404with a message naming the missing resource. - Reject rather than ignore unknown PATCH fields — explicit feedback beats silently dropping data a client believed it saved.
- Nested response objects — the list/create responses embed
clinician {first_name, last_name}andmedication {code, code_name}as required, loaded withjoinedloadto avoid N+1 queries. - Status on create — the API accepts an optional
statuson create (defaulting toactive) since the domain model lists it as a field; the overlap rule is only checked when the request is (or becomes) active. - SQLite for tests — keeps the suite dependency-free and fast (<1s). The SQL used is dialect-neutral; with more time I would run the same suite against Postgres in CI (see Future Improvements).
- Patients, Clinicians, and Medications already exist; they are populated via
the idempotent seed script (
python -m app.seed) rather than CRUD endpoints. - "Effective dates" for the overlap rule means
start_date..end_date; a nullend_dateis treated as open-ended (overlaps everything fromstart_date). - Date-only precision (no times/timezones) is sufficient for prescribing.
- Only the
completed/cancelled → activetransition is forbidden, per the brief; other transitions (e.g.completed → on-hold) are left unrestricted rather than inventing a stricter state machine. - The GET endpoint returns the full filtered list (no pagination) — acceptable at per-patient volumes; pagination is listed under future improvements.
- No authentication/authorization — out of scope for the exercise, but noted as a must-have for anything handling clinical data.
- Concurrency: the overlap check is check-then-insert and could race under
concurrent writes. In production I would add a PostgreSQL exclusion
constraint (
EXCLUDE USING giston adaterange+ patient/medication, filteredWHERE status = 'active') to enforce the rule at the database level. - Run the test suite against PostgreSQL in CI (e.g. testcontainers) in addition to SQLite.
- Pagination and sorting for the list endpoint.
- AuthN/AuthZ (OAuth2/JWT), audit logging of status transitions.
- Structured logging, request IDs, and observability (metrics/tracing).
- CI pipeline (lint, mypy, tests) via GitHub Actions.
This solution was built with Claude Code (Anthropic). I provided the requirements and made the design decisions (service-layer architecture, domain exceptions, overlap semantics, 404/409/422 mapping, test cases); Claude generated the implementation code, migration, tests, and Docker setup under my direction, and I reviewed the output. The test suite and mypy strict mode were run locally to verify the result.