notes-api¶
A personal Notes REST API in Python / FastAPI. Notes take optional tags.
Search is a case-insensitive substring. After a note is saved, an optional
background call to defense-news-classifier writes namespaced tags:
category:…, domain:… (from operational_domain), and region:….
First written in Java/Spring Boot. See decisions/ADR-001.
Tech stack¶
- Python 3.11+
- FastAPI: HTTP layer, dependency injection, BackgroundTasks
- SQLAlchemy 2.x: ORM. Tables:
notesandnote_tags. - SQLite (default, file
notes.db) or PostgreSQL (setDATABASE_URL) - Pydantic v2: request and response validation
- uv: dependency management (
pyproject.toml+uv.lock)
Architecture¶
HTTP → router.py → service.py → models.py → SQLite / PostgreSQL
↘ BackgroundTasks → classifier (CLASSIFIER_URL, optional)
router.py: FastAPI router on/notes. Wires BackgroundTasks after POST.service.py: business logic. RaisesHTTPExceptionon 404/conflict.models.py:Note+NoteTagORM entities.tagsis a list property.schemas.py: PydanticNoteRequest,TagsRequest,NoteResponse.database.py: engine + session factory.DATABASE_URLenv var.
Running it¶
uv sync # install deps
uvicorn notes_api.main:app --host ${HOST:-127.0.0.1} --port 8081 # start the server
The API listens on http://localhost:8081. Loopback is the default
(decisions/ADR-002). Set HOST=0.0.0.0 only for a separate, secured
deployment. Data persists to notes.db in the working directory. Set
DATABASE_URL for PostgreSQL:
DATABASE_URL=postgresql://user:pass@localhost/notesdb \
uvicorn notes_api.main:app --host ${HOST:-127.0.0.1} --port 8081
Set CLASSIFIER_URL to enable automatic tag enrichment after note creation:
CLASSIFIER_URL=http://localhost:8000 \
uvicorn notes_api.main:app --host ${HOST:-127.0.0.1} --port 8081
If CLASSIFIER_URL is unset, classification is skipped.
API¶
| Method | Path | Body | Status | Notes |
|---|---|---|---|---|
| GET | /notes |
200 | List notes; optional ?q= text, ?tag=, and ?published_after=/?published_before= (ISO date) filters |
|
| GET | /notes/{id} |
200 | 404 if not found | |
| POST | /notes |
NoteRequest |
201 | 400/422 if title/content blank or invalid |
| PUT | /notes/{id} |
NoteRequest |
200 | 404 if not found |
| PUT | /notes/{id}/tags |
TagsRequest |
200 | Replace tags (idempotent writeback; SYS-005) |
| DELETE | /notes/{id} |
204 | 404 if not found |
NoteRequest: { "title": "...", "content": "...", "tags": ["..."], "published_at": "2014-03-15" }.
tags and published_at are optional. published_at is the article date
(ISO 8601). Date-range filters use it. The server sets id, created_at,
updated_at, and enrichment_status.
Example¶
curl -s -X POST http://localhost:8081/notes \
-H "Content-Type: application/json" \
-d '{"title":"Cyber budget hearing","content":"Senate Armed Services Committee approved..."}'
# → 201 {"id":1,"title":"...","content":"...","tags":[],"enrichment_status":"pending","published_at":null,"created_at":"...","updated_at":"..."}
Testing¶
uv sync --group dev
uv run pytest # run tests (in-memory SQLite, no API key needed)
uv run pytest --cov=notes_api # with coverage
uv run ruff check src/ tests/ # lint
uv run black --check src/ tests/ # format check
uv run mypy src/ # type check
Tests run offline. They need no CLASSIFIER_URL and no DATABASE_URL.