A self-contained Contacts REST API built with FastAPI + SQLAlchemy, backed by an in-memory SQLite database by default. No external database, container, or migration step is needed — start the process and the API is ready.
uv venv && uv pip install -e ".[dev]" # or: python -m venv .venv && pip install -r requirements.txt
.venv/bin/python -m app.mainThen open http://127.0.0.1:8000/docs for interactive Swagger UI.
Alternatively, with uvicorn directly (adds --reload):
.venv/bin/uvicorn app.main:app --reloadFastAPI generates an OpenAPI schema from the route signatures and Pydantic models, so the docs are never out of date with the code. With the server running, three URLs are served:
| URL | What it is |
|---|---|
| http://127.0.0.1:8000/docs | Swagger UI — browse endpoints and send real requests from the browser |
| http://127.0.0.1:8000/redoc | ReDoc — read-only reference, easier for reading schemas end to end |
| http://127.0.0.1:8000/openapi.json | Raw OpenAPI 3.1 schema, for client generators and Postman/Insomnia imports |
If you changed CONTACTS_HOST or CONTACTS_PORT, substitute those instead.
- Expand an endpoint, e.g.
POST /api/v1/contacts. - Click Try it out — the request body becomes editable and is pre-filled with an example.
- Edit the JSON and click Execute.
- The response status, body, and headers appear below, along with the equivalent
curlcommand you can copy.
Since the default database is seeded on startup, GET /api/v1/contacts returns three
contacts immediately — a good first call to confirm things work. Anything you create
through the UI lives only until the process exits.
Both UIs list every model under Schemas (ReDoc) or Schemas at the bottom of the
page (Swagger UI). ContactCreate, ContactReplace (PUT), ContactUpdate (PATCH),
ContactRead, and ContactPage show exactly which fields are required, which are
nullable, and the validation rules — the same constraints described in
Contact fields below. Endpoints are grouped
by the tags declared in app/main.py, and each documents its error responses (404,
409, 422) with example payloads.
Neither UI requires the docs to be enabled explicitly; to turn them off in a deployment,
pass docs_url=None / redoc_url=None to FastAPI(...) in app/main.py.
CONTACTS_DATABASE_URL defaults to sqlite+pysqlite:///:memory:. A plain in-memory
SQLite database normally dies with the connection that opened it, so app/database.py
uses SQLAlchemy's StaticPool to hold one connection open for the process's lifetime.
Every request — including ones FastAPI runs on a worker thread — sees the same data.
Data is lost when the process exits. Because of that, three sample contacts are seeded on startup so the API is never empty. To persist instead, point at a file:
CONTACTS_DATABASE_URL="sqlite+pysqlite:///./contacts.db" .venv/bin/python -m app.mainThe same code runs unchanged against Postgres (postgresql+psycopg://...).
All settings are environment variables prefixed with CONTACTS_ (a .env file is
also read):
| Variable | Default | Purpose |
|---|---|---|
CONTACTS_DATABASE_URL |
sqlite+pysqlite:///:memory: |
SQLAlchemy URL |
CONTACTS_SEED_DATA |
true |
Insert sample contacts if the DB is empty |
CONTACTS_HOST |
127.0.0.1 |
Bind address |
CONTACTS_PORT |
8000 |
Bind port |
CONTACTS_SQL_ECHO |
false |
Log every SQL statement |
| Method | Path | Description |
|---|---|---|
GET |
/health |
Liveness + database check and contact count |
GET |
/ |
Entry-point listing |
POST |
/api/v1/contacts |
Create a contact → 201 |
GET |
/api/v1/contacts |
List with search, sort, pagination |
GET |
/api/v1/contacts/{id} |
Fetch one contact |
PUT |
/api/v1/contacts/{id} |
Full replace (omitted fields are cleared) |
PATCH |
/api/v1/contacts/{id} |
Partial update (only sent fields change) |
DELETE |
/api/v1/contacts/{id} |
Delete → 204 |
first_name and last_name are required; email is required and unique
(case-insensitive). Everything else is optional.
first_name, last_name, email, phone, company, job_title,
address, city, state, postal_code, country, notes
Responses add id, full_name, created_at, and updated_at (UTC).
| Param | Default | Notes |
|---|---|---|
search |
– | Case-insensitive substring match on name, email, company, phone |
limit |
50 |
1–200 |
offset |
0 |
|
sort_by |
id |
id, first_name, last_name, email, company, created_at, updated_at |
order |
asc |
asc or desc |
List responses are wrapped so clients can paginate:
{ "items": [ ... ], "total": 12, "limit": 50, "offset": 0 }201 created · 204 deleted · 404 unknown id · 409 duplicate email ·
422 validation error (bad email, blank name, invalid sort_by)
# Create
curl -X POST http://127.0.0.1:8000/api/v1/contacts \
-H 'content-type: application/json' \
-d '{"first_name":"Katherine","last_name":"Johnson","email":"katherine@example.com",
"phone":"+1-757-555-0199","company":"NASA","job_title":"Mathematician"}'
# Search + paginate
curl "http://127.0.0.1:8000/api/v1/contacts?search=nasa&limit=10&sort_by=last_name"
# Partial update
curl -X PATCH http://127.0.0.1:8000/api/v1/contacts/1 \
-H 'content-type: application/json' -d '{"phone":"+1-415-555-0000"}'
# Delete
curl -X DELETE http://127.0.0.1:8000/api/v1/contacts/1.venv/bin/python -m pytestTests run against their own empty in-memory database with seeding disabled
(see tests/conftest.py).
app/
main.py FastAPI app, lifespan startup, /health and /
config.py Environment-driven settings
database.py Engine, session factory, StaticPool in-memory wiring
models.py Contact ORM model
schemas.py Pydantic request/response models
crud.py Database operations (search, sort, paginate)
seed.py Sample contacts for the in-memory default
routers/contacts.py REST endpoints
tests/ API tests via FastAPI TestClient