Monastery Web API is a productionβoriented, containerized RESTful web service for managing monastery records. It is built with Flask, SQLAlchemy (ORM) and MySQL, and ships with versioned database migrations, unit and integration tests, interactive Swagger documentation, and a Docker Compose setup that makes local development and CI fully reproducible.
The project is intentionally small in domain but complete in engineering: layered structure, typed error handling, schema migrations, automated tests, and a oneβcommand deployment.
TL;DR β
docker compose up -d --buildand open http://localhost:5000/apidocs.
- Key Features
- Tech Stack
- Architecture
- Quick Start
- Configuration
- Database & Migrations
- API Reference
- Example Requests
- Error Handling
- Testing
- Project Structure
- Docker Deep Dive
- Troubleshooting
- Security Notes
- Roadmap
- Contributing
- License
| Feature | Details | |
|---|---|---|
| π | RESTful CRUD | Full resource lifecycle: GET, POST, PUT, PATCH, DELETE |
| π§± | ORM Model Layer | SQLAlchemy model with validation and helpers: save, update, delete, to_dict |
| 𧬠| Versioned Migrations | FlaskβMigrate (Alembic) for safe, reviewable schema evolution |
| π | Interactive Docs | Swagger UI via Flasgger at /apidocs |
| π¨ | Consistent Errors | Typed exceptions mapped to structured JSON responses, with logging |
| π§ͺ | Automated Tests | Unit tests (model) and integration tests (API routes) with unittest |
| π³ | Containerized | Dockerfile + docker-compose.yml, served by Gunicorn |
| πΎ | Persistent Storage | Named Docker volume keeps MySQL data across restarts |
| Layer | Technology | Purpose |
|---|---|---|
| Language | Python 3.11 | Application runtime |
| Web framework | Flask | HTTP routing and request handling |
| ORM | SQLAlchemy | Objectβrelational mapping |
| Database | MySQL (pymysql driver) |
Primary datastore |
| Migrations | FlaskβMigrate / Alembic | Versioned schema changes |
| API docs | Flasgger (Swagger UI) | Interactive documentation |
| WSGI server | Gunicorn | Productionβgrade serving |
| Containers | Docker & Docker Compose | Reproducible environments |
| Testing | unittest | Unit + integration test suites |
flowchart LR
subgraph Client["π₯οΈ Presentation Layer"]
C1["Browser / Swagger UI"]
C2["curl / Postman / Services"]
end
subgraph API["βοΈ Business Logic Layer β container: monastery_api"]
G["Gunicorn (WSGI)"]
F["Flask app (app.py)"]
E["exceptions.py<br/>typed errors β JSON"]
G --> F
F --> E
end
subgraph DATA["ποΈ Persistence Layer"]
M["Models/Monastery.py<br/>SQLAlchemy ORM"]
DB[("MySQL<br/>container: monastery_db")]
V[("Docker volume")]
M --> DB
DB --- V
end
C1 -->|HTTP/JSON| G
C2 -->|HTTP/JSON| G
F --> M
style Client fill:#0d1117,stroke:#2F81F7,color:#fff
style API fill:#0d1117,stroke:#3FB950,color:#fff
style DATA fill:#0d1117,stroke:#F0883E,color:#fff
sequenceDiagram
autonumber
actor Client
participant G as Gunicorn
participant F as Flask (app.py)
participant M as Monastery model
participant DB as MySQL
Client->>G: POST /api/monasteries (JSON)
G->>F: WSGI request
F->>M: validate payload
alt valid
M->>DB: INSERT
DB-->>M: row + id
M-->>F: to_dict()
F-->>Client: 201 Created + JSON
else invalid
M-->>F: ValidationError(details)
F-->>Client: 400 Bad Request + structured error
end
erDiagram
MONASTERY {
int id PK "auto-increment"
string name "required, non-empty"
string location "required"
int year_of_construction "required"
}
- Separation of concerns β routing (
app.py), persistence (Models/) and error semantics (exceptions.py) live in separate modules. - Fail loudly, respond consistently β every error path produces the same JSON envelope and is logged to
errors.log. - Reproducibility first β the whole stack (API + database) is defined declaratively in Compose.
- Schema as code β database changes are tracked as Alembic migrations, never applied by hand.
- Portable by default β configuration through environment variables keeps the service cloudβready.
- Docker 20.10+
- Docker Compose v2 (
docker compose) git
git clone https://github.com/MIhajloS07/Monastery-Web-API.git
cd Monastery-Web-APIdocker compose up -d --builddocker compose psYou should see both monastery_api and monastery_db in the running state.
docker compose exec monastery_api flask db upgrade| Resource | URL |
|---|---|
| API base URL | http://localhost:5000/api/monasteries |
| Swagger UI | http://localhost:5000/apidocs |
curl http://localhost:5000/api/monasteries# stop and remove containers, keep database data
docker compose down
# stop and remove containers AND volumes (destructive β deletes all data)
docker compose down -vConfiguration is supplied through environment variables.
| Variable | Description | Where defined |
|---|---|---|
SQLALCHEMY_DATABASE_URI |
SQLAlchemy connection string | docker-compose.yml / .env |
MYSQL_DATABASE |
Database name | docker-compose.yml / .env |
MYSQL_USER |
Application DB user | docker-compose.yml / .env |
MYSQL_PASSWORD |
Application DB password | docker-compose.yml / .env |
MYSQL_ROOT_PASSWORD |
MySQL root password | docker-compose.yml / .env |
When running under Compose, the host in the URI must be the Compose service name of the database container (not localhost):
mysql+pymysql://<user>:<password>@<db-service-name>:3306/<database>
- Use a
.envfile or CI secrets to override sensitive values. - Never commit real credentials. Commit a
.env.examplewith safe placeholders instead:
# .env.example
MYSQL_DATABASE=monastery
MYSQL_USER=monastery_user
MYSQL_PASSWORD=change_me
MYSQL_ROOT_PASSWORD=change_me_too
SQLALCHEMY_DATABASE_URI=mysql+pymysql://monastery_user:change_me@monastery_db:3306/monasterySchema changes are managed with FlaskβMigrate (Alembic).
flowchart LR
A["Edit model<br/>Models/Monastery.py"] --> B["flask db migrate -m '...'"]
B --> C["Review generated<br/>migrations/versions/*.py"]
C --> D["flask db upgrade"]
D --> E[("MySQL schema updated")]
style A fill:#161b22,stroke:#2F81F7,color:#fff
style B fill:#161b22,stroke:#2F81F7,color:#fff
style C fill:#161b22,stroke:#F0883E,color:#fff
style D fill:#161b22,stroke:#3FB950,color:#fff
style E fill:#161b22,stroke:#3FB950,color:#fff
# 1) generate a migration after changing a model
docker compose exec monastery_api flask db migrate -m "describe change"
# 2) ALWAYS review the generated file in migrations/versions/
# 3) apply migrations
docker compose exec monastery_api flask db upgrade
# roll back one revision
docker compose exec monastery_api flask db downgrade
# inspect history / current revision
docker compose exec monastery_api flask db history
docker compose exec monastery_api flask db currentBase URL: http://localhost:5000/api/monasteries
Content type: application/json
| Method | Endpoint | Description | Success | Errors |
|---|---|---|---|---|
GET |
/api/monasteries |
List all monasteries | 200 |
β |
GET |
/api/monasteries/{id} |
Get a monastery by ID | 200 |
404 |
POST |
/api/monasteries |
Create a monastery | 201 |
400 |
PUT |
/api/monasteries/{id} |
Replace a monastery (full update) | 200 |
400, 404 |
PATCH |
/api/monasteries/{id} |
Partially update a monastery | 200 |
400, 404 |
DELETE |
/api/monasteries/{id} |
Delete a monastery | 204 |
404 |
| Field | Type | Required | Description |
|---|---|---|---|
id |
integer | β (readβonly) | Autoβgenerated primary key |
name |
string | β | Monastery name, must not be empty |
location |
string | β | Place / region, e.g. "Kraljevo, Serbia" |
year_of_construction |
integer | β | Year the monastery was built |
| Code | Meaning | When |
|---|---|---|
200 |
OK | Successful read or update |
201 |
Created | Resource successfully created |
204 |
No Content | Resource successfully deleted |
400 |
Bad Request | Validation failed |
404 |
Not Found | Resource does not exist |
500 |
Internal Server Error | Unexpected failure (logged to errors.log) |
π§ͺ Prefer clicking over typing? Open Swagger UI to explore schemas and execute requests directly from the browser.
β Create a monastery β POST
curl -X POST http://localhost:5000/api/monasteries \
-H "Content-Type: application/json" \
-d '{
"name": "Studenica",
"location": "Kraljevo, Serbia",
"year_of_construction": 1190
}'Response 201 Created
{
"id": 1,
"name": "Studenica",
"location": "Kraljevo, Serbia",
"year_of_construction": 1190
}π List monasteries β GET
curl http://localhost:5000/api/monasteriesResponse 200 OK
[
{
"id": 1,
"name": "Studenica",
"location": "Kraljevo, Serbia",
"year_of_construction": 1190
},
{
"id": 2,
"name": "SopoΔani",
"location": "RaΕ‘ka, Serbia",
"year_of_construction": 1260
}
]π Get by ID β GET
curl http://localhost:5000/api/monasteries/1Response 200 OK
{
"id": 1,
"name": "Studenica",
"location": "Kraljevo, Serbia",
"year_of_construction": 1190
}Response 404 Not Found
{ "error": "Monastery not found", "status": 404 }βοΈ Full update β PUT
curl -X PUT http://localhost:5000/api/monasteries/1 \
-H "Content-Type: application/json" \
-d '{
"name": "Studenica",
"location": "Kraljevo, Serbia",
"year_of_construction": 1190
}'Response 200 OK
{
"id": 1,
"name": "Studenica",
"location": "Kraljevo, Serbia",
"year_of_construction": 1190
}π©Ή Partial update β PATCH
curl -X PATCH http://localhost:5000/api/monasteries/1 \
-H "Content-Type: application/json" \
-d '{"location": "New Location, Serbia"}'Response 200 OK
{
"id": 1,
"name": "Studenica",
"location": "New Location, Serbia",
"year_of_construction": 1190
}ποΈ Delete β DELETE
curl -X DELETE http://localhost:5000/api/monasteries/1Response 204 No Content (empty body)
β οΈ Validation error β 400
curl -X POST http://localhost:5000/api/monasteries \
-H "Content-Type: application/json" \
-d '{"name": "", "location": "X"}'Response 400 Bad Request
{
"error": "Validation failed",
"details": {
"name": "Name must not be empty",
"year_of_construction": "Year is required"
},
"status": 400
}All errors share one predictable envelope, produced by the typed exceptions in exceptions.py:
{
"error": "Human-readable message",
"details": { "field": "optional per-field explanation" },
"status": 400
}| Field | Type | Always present | Description |
|---|---|---|---|
error |
string | β | Short description of what went wrong |
status |
integer | β | HTTP status code mirrored in the body |
details |
object | β | Perβfield validation messages (validation errors only) |
Errors are also written to errors.log for postβmortem analysis.
The suite is split by scope:
| File | Scope | Covers |
|---|---|---|
tests/test_monastery_model.py |
Unit | Model validation and helper methods |
tests/test_api.py |
Integration | Routes, status codes, payloads, error responses |
docker compose exec monastery_api python -m unittest discover -s testsflowchart LR
A["1. Build & start<br/>docker compose up -d --build"] --> B["2. Apply migrations<br/>flask db upgrade"]
B --> C["3. Run tests<br/>python -m unittest discover -s tests"]
C --> D["4. Tear down (optional)<br/>docker compose down -v"]
style A fill:#161b22,stroke:#2F81F7,color:#fff
style B fill:#161b22,stroke:#F0883E,color:#fff
style C fill:#161b22,stroke:#3FB950,color:#fff
style D fill:#161b22,stroke:#F85149,color:#fff
docker compose up -d --build
docker compose exec monastery_api flask db upgrade
docker compose exec monastery_api python -m unittest discover -s tests
docker compose down -v # optional: wipe the ephemeral test databasedocker compose exec monastery_api python -m unittest tests.test_api -vπ Monastery-Web-API/
βββ π migrations/ # Alembic migrations
β βββ π versions/ # Database migration history
β βββ alembic.ini # Alembic configuration
β βββ env.py # Migration environment setup
β βββ script.py.mako # Template for generated migrations
β
βββ π Models/ # Persistence layer (ORM)
β βββ __init__.py # Package initializer
β βββ Monastery.py # Monastery entity model
β
βββ π tests/ # Unit and integration tests
β βββ test_api.py # API route tests
β βββ test_monastery_model.py # Model tests
β
βββ app.py # Flask app, routes, Swagger annotations
βββ exceptions.py # Typed exceptions and JSON error handling
βββ requirements.txt # Python dependencies
βββ Dockerfile # Image build for monastery_api
βββ docker-compose.yml # Orchestration: API + MySQL
βββ .env # Environment variables (keep out of VCS)
βββ .dockerignore # Files excluded from the Docker build context
βββ .gitignore # Files excluded from Git
βββ errors.log # Runtime error log
βββ LICENSE # MIT license
βββ README.md # You are here
| Component | Layer | Responsibility |
|---|---|---|
app.py |
Business logic | Routes, request parsing, Swagger docs |
Models/Monastery.py |
Persistence | Schema, validation, save / update / delete / to_dict |
exceptions.py |
Crossβcutting | Typed errors, logging, uniform JSON payloads |
migrations/ |
Persistence | Versioned schema history |
tests/ |
Quality | Automated verification |
Dockerfile, docker-compose.yml |
Infrastructure | Build, run and wire the services |
flowchart TB
subgraph Host["π» Host machine"]
P["localhost:5000"]
end
subgraph Net["π Compose network"]
subgraph API["monastery_api"]
GU["Gunicorn β Flask"]
end
subgraph DBC["monastery_db"]
MY["MySQL :3306"]
end
GU -->|"pymysql"| MY
end
VOL[("π¦ Named volume<br/>MySQL data")]
P -->|"port 5000"| GU
MY --- VOL
style Host fill:#0d1117,stroke:#8b949e,color:#fff
style Net fill:#0d1117,stroke:#2F81F7,color:#fff
style API fill:#161b22,stroke:#3FB950,color:#fff
style DBC fill:#161b22,stroke:#F0883E,color:#fff
| Service | Role | Notes |
|---|---|---|
monastery_api |
Flask app served by Gunicorn | Exposed on host port 5000 |
monastery_db |
MySQL database | Data persisted in a named Docker volume |
docker compose logs -f monastery_api # follow API logs
docker compose logs -f monastery_db # follow DB logs
docker compose exec monastery_api bash # shell into the API container
docker compose restart monastery_api # restart only the API
docker compose build --no-cache # force a clean rebuildAPI cannot connect to the database
- Make sure
SQLALCHEMY_DATABASE_URIuses the Compose service name of the database container as host, notlocalhost. - The database may still be initializing on first start. Wait a few seconds and check
docker compose logs monastery_db.
Table monasteries doesn't exist / 500 on first request
Migrations have not been applied yet:
docker compose exec monastery_api flask db upgradePort 5000 is already in use
Change the hostβside port mapping in docker-compose.yml (for example "5001:5000") and use http://localhost:5001 instead. On macOS, AirPlay Receiver commonly occupies port 5000.
I want a completely clean slate
docker compose down -v
docker compose up -d --build
docker compose exec monastery_api flask db upgrade- Keep
.envout of version control; ship a.env.examplewith placeholders only. - Replace every default password before any nonβlocal deployment.
- Do not expose the MySQL port publicly; keep it reachable only on the internal Compose network.
- Run behind a reverse proxy (e.g. Nginx / Traefik) with TLS in production.
- Consider adding authentication, rate limiting and CORS policy before exposing the API publicly (see Roadmap).
- Pagination, filtering and sorting on
GET /api/monasteries - CI pipeline (GitHub Actions): build image, run migrations, run tests on every push
- Test coverage reporting with a coverage badge
-
.env.exampleand aMakefilefor common tasks (make up,make test,make migrate,make seed) - Seed script with real Serbian monastery data
- Authentication (JWT) and roleβbased write access
- Container health checks and
depends_on: condition: service_healthy - Request validation with schemas (e.g. Marshmallow / Pydantic)
- Structured JSON logging and request IDs
- Linting and formatting (Ruff / Black) with preβcommit hooks
Contributions, issues and feature requests are welcome.
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m "feat: add amazing feature" - Run the tests and make sure they pass
- Push the branch:
git push origin feature/amazing-feature - Open a Pull Request
Commit messages follow Conventional Commits (feat:, fix:, docs:, test:, refactor:, chore:).
Distributed under the MIT License. See the LICENSE file for full terms.
