Sample backend API built with FastAPI, SQLAlchemy 2.x, and Alembic. It demonstrates a layered architecture with full CRUD endpoints for Person and read-only list/detail endpoints for Country and Continent.
- Python 3.12+
- uv
- SQLite
-
Install dependencies
uv sync --locked --all-extras --dev -
Copy
.env.exampleto.envand change the values if needed
cp .env.example .env -
Run the app
uv run python run.py -
Call the API endpoint
curl http://localhost:5000/persons -
Open
http://localhost:5000/docsto view the OpenAPI docs
-
Build the image
docker build -t pyfastapi . -
Run the image
docker run -p 5000:5000 pyfastapi
Note: You can use
podmaninstead ofdockeras it is a drop-in replacement.
-
Run migrations
alembic upgrade head -
The SQL schema and seed data will be created in
./sql_app.db -
The database URL is read from
.env(or.env.testwhenENVIRONMENT=test) viapyfastapi/utils/config.pyand passed to Alembic inalembic/env.py.alembic.iniis used for other Alembic settings.
-
Make sure dependencies are installed
uv sync --locked --all-extras --dev -
Run the test suite
uv run pytest -
Run with coverage
uv run poe coverage
Tests automatically set
ENVIRONMENT=test, which loads.env.testfor test configuration.
- No trailing slashes on collection endpoints. Routes are registered as
/persons,/countries, and/continents. Accessing the trailing-slash variant (e.g.,/persons/) will receive a307 Temporary Redirectto the canonical path. - Sort syntax (
/personsand/countriesonly): Prefix with-for descending (e.g.,-name),+or no prefix for ascending. - Filter syntax (
/personsand/countriesonly): Query parameters are mapped to schema fields. String fields useILIKE(%value%); others use exact equality. GET /continentsreturns a plainList[ContinentSchema]; it is not paginated and does not support sort/filter parameters.POST /personscreates a person and returnsPersonListSchema(which includescountry_codebut not the nested country/continent).PUT /persons/{id}is an upsert. If the ID exists it is updated; if it does not exist a new person is created with that ID. Returns204 No Content.DELETE /persons/{id}removes the person and returns204 No Content. Returns404 Not Foundif the person does not exist.
---
config:
layout: dagre
look: handDrawn
title: ERD
---
erDiagram
persons {
int id PK
String last_name "nullable"
String first_name
String country_code FK
}
countries {
String code PK
String name
int phone
String symbol "nullable"
String capital "nullable"
String currency "nullable"
String continent_code FK "nullable"
String alpha_3 "nullable"
}
continents {
String code PK
String name
}
persons }|--|| countries : "country"
countries }|--|| continents : "continent"