Skip to content

About

Monastery CRUD API built with Flask, MySQL, and Swagger UI, containerized with Docker and tested using Python unit tests, following REST API design principles.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

Β 

History

5 Commits

Folders and files

Repository files navigation

Monastery Web API banner Typing SVG

Python Flask SQLAlchemy MySQL Docker Swagger Gunicorn

License: MIT Repo size Last commit Stars Issues PRs Welcome


Tech stack icons



Quick Start Β β€’Β  API Reference Β β€’Β  Architecture Β β€’Β  Testing Β β€’Β  Roadmap


πŸ“– Overview

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 --build and open http://localhost:5000/apidocs.


πŸ“‘ Table of Contents


✨ Key Features

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

🧰 Tech Stack

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

πŸ› Architecture

Layered overview

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
Loading

Request lifecycle

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
Loading

Data model

erDiagram
    MONASTERY {
        int id PK "auto-increment"
        string name "required, non-empty"
        string location "required"
        int year_of_construction "required"
    }
Loading
πŸ–ΌοΈ Original architecture diagram
Monastery Web API architecture diagram

Design principles

  • 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.

πŸš€ Quick Start

Prerequisites

1. Clone

git clone https://github.com/MIhajloS07/Monastery-Web-API.git
cd Monastery-Web-API

2. Build and run (detached)

docker compose up -d --build

3. Verify services

docker compose ps

You should see both monastery_api and monastery_db in the running state.

4. Apply database migrations

docker compose exec monastery_api flask db upgrade

5. Open the API

Resource URL
API base URL http://localhost:5000/api/monasteries
Swagger UI http://localhost:5000/apidocs

6. Smoke test

curl http://localhost:5000/api/monasteries

Stop the stack

# stop and remove containers, keep database data
docker compose down

# stop and remove containers AND volumes (destructive β€” deletes all data)
docker compose down -v

βš™οΈ Configuration

Configuration 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

Connection string

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>

Local overrides

  • Use a .env file or CI secrets to override sensitive values.
  • Never commit real credentials. Commit a .env.example with 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/monastery

πŸ—„οΈ Database & Migrations

Schema 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
Loading
# 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 current

πŸ“‘ API Reference

Base URL: http://localhost:5000/api/monasteries Content type: application/json

Endpoints

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

Resource schema

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

HTTP status codes

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.


🧾 Example Requests

βž• 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/monasteries

Response 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/1

Response 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/1

Response 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
}

🚨 Error Handling

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.


πŸ§ͺ Testing

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

Run the full suite

docker compose exec monastery_api python -m unittest discover -s tests

Recommended local workflow

flowchart 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
Loading
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 database

Run a single test module

docker compose exec monastery_api python -m unittest tests.test_api -v

πŸ—‚ Project Structure

πŸ“‚ 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

Responsibility map

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

🐳 Docker Deep Dive

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
Loading
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

Useful commands

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 rebuild

🩺 Troubleshooting

API cannot connect to the database
  • Make sure SQLALCHEMY_DATABASE_URI uses the Compose service name of the database container as host, not localhost.
  • 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 upgrade
Port 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

⚠️ This deletes all stored data.


πŸ” Security Notes

  • Keep .env out of version control; ship a .env.example with 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).

πŸ›£ 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.example and a Makefile for 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

🀝 Contributing

Contributions, issues and feature requests are welcome.

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/amazing-feature
  3. Commit your changes: git commit -m "feat: add amazing feature"
  4. Run the tests and make sure they pass
  5. Push the branch: git push origin feature/amazing-feature
  6. Open a Pull Request

Commit messages follow Conventional Commits (feat:, fix:, docs:, test:, refactor:, chore:).


πŸ“„ License

License: MIT

Distributed under the MIT License. See the LICENSE file for full terms.


πŸ‘€ Author

MIhajloS07

⭐ If you find this project useful, consider giving it a star β€” it helps a lot!

footer wave

About

Monastery CRUD API built with Flask, MySQL, and Swagger UI, containerized with Docker and tested using Python unit tests, following REST API design principles.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages