Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/web-framework-examples.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Test optional web framework examples

on:
push:
paths:
- "examples/web-frameworks/**"
- ".github/workflows/web-framework-examples.yml"
pull_request:
paths:
- "examples/web-frameworks/**"
- ".github/workflows/web-framework-examples.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
test:
name: Python 3.12 / ${{ matrix.framework }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
framework: [fastapi, flask, django]
defaults:
run:
working-directory: examples/web-frameworks/${{ matrix.framework }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: python -m pip install -r requirements.txt
- name: Test FastAPI and Flask
if: matrix.framework != 'django'
run: python -m unittest discover -s tests -v
- name: Test Django and migrations
if: matrix.framework == 'django'
run: |
python manage.py check
python manage.py makemigrations --check --dry-run
python manage.py test books -v 2
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Primary lab environment: **Google Colab**
- Ready to run code: [Open notebooks in Google Colab](COLAB.md)
- Need the course readings: [Reading & Reference Access Guide](READING_GUIDE.md)
- Want the full pathway: [Course Map](COURSE_MAP.md)
- Optional after learning OOP: [FastAPI, Flask, and Django mini-projects](examples/web-frameworks/README.md)

No enrollment or fixed calendar is required.

Expand Down
172 changes: 172 additions & 0 deletions examples/web-frameworks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Bonus praktikum: FastAPI, Flask, dan Django

> **Materi tambahan (opsional)** untuk pembelajar yang sudah memahami OOP.
> Web framework, HTTP/REST, database, dan dependency eksternal **bukan**
> capaian wajib atau bahan asesmen modul 0–16. Lihat [Source Boundary](../../SOURCE_BOUNDARY.md).

Ketiga proyek menjalankan **Book Management REST API** yang sama sehingga
perbandingan berfokus pada cara setiap framework menerima HTTP request,
memproses business logic, dan menghasilkan HTTP response.

## Tujuan pembelajaran

- Menghubungkan class, object, encapsulation, state, dan responsibility dengan backend web.
- Melihat perbedaan routing, request parsing, validation, persistence, dan pengujian.
- Memahami bahwa framework bukan pengganti desain OOP.

## Struktur

```text
examples/web-frameworks/
├── README.md # Panduan komparatif ini
├── fastapi/ # Type hints, Pydantic, OpenAPI, in-memory repository
├── flask/ # Minimal routing, validasi manual, in-memory repository
└── django/ # Model ORM, migration, SQLite, view JSON native
```

## API yang dibandingkan (ketiga proyek sama)

| Method | Endpoint | Hasil | Status sukses |
|---|---|---|---|
| GET | /books | Semua buku | 200 |
| GET | /books/{id} | Satu buku | 200 |
| POST | /books | Membuat buku | 201 |
| PUT | /books/{id} | Mengubah seluruh field buku | 200 |
| DELETE | /books/{id} | Menghapus buku | 204 |

Format input untuk POST dan PUT:

```json
{"title": "Clean Code", "author": "Robert C. Martin"}
```

Format objek output:

```json
{"id": 1, "title": "Clean Code", "author": "Robert C. Martin"}
```

Seluruh field `title` dan `author` wajib berupa teks yang tidak kosong
(setelah whitespace dibuang), maksimal 120 karakter. ID dibuat server.
Respons input tidak valid adalah 422 (FastAPI) atau 400 (Flask/Django).
ID tidak ditemukan menghasilkan 404.

## Cara cepat menjalankan

Gunakan **Python 3.12+**. Jalankan dari direktori proyek masing-masing,
dan buat virtual environment terpisah agar dependensi tidak saling bertabrakan.

**FastAPI** — [baca panduan](fastapi/README.md):

```bash
cd examples/web-frameworks/fastapi
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install -r requirements.txt
uvicorn app:app --reload --port 8001
```

Buka http://127.0.0.1:8001/docs untuk dokumentasi interaktif.

**Flask** — [baca panduan](flask/README.md):

```bash
cd examples/web-frameworks/flask
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
flask --app app run --debug --port 8002
```

**Django** — [baca panduan](django/README.md):

```bash
cd examples/web-frameworks/django
python -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt
python manage.py migrate
python manage.py runserver 8003
```

Di terminal terpisah, uji salah satu server (ganti port 8001 menjadi 8002/8003):

```bash
curl -i http://127.0.0.1:8001/books
curl -i -X POST http://127.0.0.1:8001/books \
-H "Content-Type: application/json" \
-d '{"title":"Clean Code","author":"Robert C. Martin"}'
curl -i http://127.0.0.1:8001/books/1
curl -i -X PUT http://127.0.0.1:8001/books/1 \
-H "Content-Type: application/json" \
-d '{"title":"Refactoring","author":"Martin Fowler"}'
curl -i -X DELETE http://127.0.0.1:8001/books/1
```

### Menjalankan tes

Jalankan dari direktori tiap proyek, setelah memasang `requirements.txt`:

```bash
# FastAPI / Flask
python -m unittest discover -s tests -v

# Django
python manage.py test books -v 2
```

GitHub Actions juga menjalankan ketiga suite melalui
[`web-framework-examples.yml`](../../.github/workflows/web-framework-examples.yml).

## Perbandingan arsitektur

```text
Client (curl/browser)
|
v
HTTP router / endpoint <-- FastAPI / Flask / Django
|
v
Business objects & rules <-- Book / BookRepository atau Book ORM
|
v
Storage <-- dict in-memory atau SQLite
|
v
JSON response + HTTP status
```

| Aspek | FastAPI | Flask | Django |
|---|---|---|---|
| Pemetaan URL | Decorator `@app.get/post/put/delete` | Decorator `@app.get/post/put/delete` | `urls.py` |
| Validasi input | `Pydantic` | Manual pada handler | Manual pada handler (tanpa Django REST Framework) |
| Model/domain | Dataclass `Book` | Dataclass `Book` | `models.Model` |
| Penyimpanan contoh | Dictionary dalam `BookRepository` | Dictionary dalam `BookRepository` | SQLite melalui ORM & migration |
| Dokumentasi OpenAPI | Otomatis di `/docs` | Tidak otomatis | Tidak otomatis |
| Konsep OOP | Composition: handler memakai repository | Composition: handler memakai repository | Model adalah class berbasis ORM |

**Catatan tentang keadilan perbandingan:** perbedaan penyimpanan disengaja.
FastAPI/Flask memakai state proses yang hilang saat server restart; Django
memakai SQLite yang bertahan setelah restart. Karena itu contoh ini **bukan**
benchmark performa maupun rekomendasi arsitektur produksi.

## Tugas praktikum lanjutan (opsional)

1. Tambahkan atribut `year` pada tiga implementasi dan ubah pengujiannya.
2. Jelaskan perbedaan validasi FastAPI, Flask, dan Django.
3. Ubah Flask/FastAPI agar menyimpan data ke database; bandingkan tanggung jawab handler.
4. Tambahkan autentikasi, otorisasi, rate limiting, dan konfigurasi aman jika proyek dikembangkan ke produksi.

## Peringatan keamanan dan operasional

Semua contoh disediakan untuk **belajar di lokal**: tanpa autentikasi, tanpa
otorisasi, tanpa pagination, tanpa audit log, dan tanpa deployment hardening.
Penggunaan untuk produksi memerlukan konfigurasi rahasia dari environment,
penanganan konkurensi/transaksi, database yang sesuai, dan proteksi request
(termasuk CSRF bila menggunakan autentikasi berbasis cookie).

## Bacaan dokumentasi resmi

- FastAPI: https://fastapi.tiangolo.com/
- Flask: https://flask.palletsprojects.com/
- Django: https://docs.djangoproject.com/
2 changes: 2 additions & 0 deletions examples/web-frameworks/django/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
db.sqlite3
db.sqlite3-journal
59 changes: 59 additions & 0 deletions examples/web-frameworks/django/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Django: Book Management REST API

Django memakai `Book(models.Model)` yang sekaligus mendeskripsikan data
dan menjadi pintu masuk **ORM**. Data contoh tersimpan dalam **SQLite**.
Untuk perbandingan langsung dengan FastAPI/Flask, HTTP API dibuat memakai
**Django native views**, tanpa Django REST Framework.

## Arsitektur

```text
HTTP request
-> config/urls.py -> books/urls.py
-> books/views.py (JSON parsing & validasi)
-> books/models.py: Book(models.Model)
-> Django ORM -> SQLite (db.sqlite3)
-> JsonResponse()
```

**Konsep OOP:** `Book` adalah subclass dari `models.Model`
(**inheritance**); instansinya menggabungkan state dan behavior seperti
`save()` / `delete()`. Views memakai objek ORM, bukan menulis SQL manual.

## Menjalankan (Python 3.12+)

```bash
cd examples/web-frameworks/django
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install -r requirements.txt
python manage.py migrate
python manage.py runserver 8003
```

- API: http://127.0.0.1:8003/books
- Pengujian: `python manage.py test books -v 2` (database tes terisolasi).
- Cek perubahan model: `python manage.py makemigrations --check --dry-run`.

Contoh:

```bash
curl -i -X POST http://127.0.0.1:8003/books \
-H "Content-Type: application/json" \
-d '{"title":"Clean Code","author":"Robert C. Martin"}'
curl -i http://127.0.0.1:8003/books/1
```

## Apa yang perlu diperhatikan?

- `manage.py migrate` harus dijalankan sebelum memakai API.
- Model menyatakan field; migrasi `0001_initial.py` membuat tabel.
- Validasi JSON diprogram manual; tanpa DRF tidak ada serializer otomatis
dan error input contoh ini menghasilkan **400**.
- Django mendukung fitur yang lebih luas daripada yang ditampilkan oleh
proyek mini ini, seperti autentikasi, permissions, forms, admin, dan ORM.
- View `POST/PUT/DELETE` di sini menggunakan `csrf_exempt` **hanya untuk
demonstrasi JSON API tanpa login/cookie**. Jangan memakai pendekatan ini
untuk endpoint produksi yang mengandalkan sesi/cookie.
- `SECRET_KEY`, `DEBUG=True`, dan SQLite di sini untuk lokal saja;
bukan konfigurasi deployment.
1 change: 1 addition & 0 deletions examples/web-frameworks/django/books/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Books Django app.
6 changes: 6 additions & 0 deletions examples/web-frameworks/django/books/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class BooksConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "books"
18 changes: 18 additions & 0 deletions examples/web-frameworks/django/books/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="Book",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("title", models.CharField(max_length=120)),
("author", models.CharField(max_length=120)),
],
)
]
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Django database migrations.
11 changes: 11 additions & 0 deletions examples/web-frameworks/django/books/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""A Django ORM class: combines domain data with database persistence."""

from django.db import models


class Book(models.Model):
title = models.CharField(max_length=120)
author = models.CharField(max_length=120)

def __str__(self):
return f"{self.title} — {self.author}"
60 changes: 60 additions & 0 deletions examples/web-frameworks/django/books/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import json

from django.test import TestCase

from .models import Book


class BookApiTests(TestCase):
def send_json(self, method, path, payload):
return getattr(self.client, method)(
path,
data=json.dumps(payload),
content_type="application/json",
)

def test_complete_crud(self):
self.assertEqual(self.client.get("/books").json(), [])
created = self.send_json(
"post", "/books", {"title": " Clean Code ", "author": " Robert C. Martin "}
)
self.assertEqual(created.status_code, 201)
self.assertEqual(
created.json(), {"id": 1, "title": "Clean Code", "author": "Robert C. Martin"}
)
self.assertEqual(self.client.get("/books/1").json(), created.json())
self.assertEqual(Book.objects.count(), 1)

updated = self.send_json(
"put", "/books/1", {"title": "Refactoring", "author": "Martin Fowler"}
)
self.assertEqual(updated.status_code, 200)
self.assertEqual(updated.json()["title"], "Refactoring")
self.assertEqual(self.client.delete("/books/1").status_code, 204)
self.assertEqual(self.client.get("/books").json(), [])

def test_input_validation(self):
for payload in [
{"title": " ", "author": "Jane"},
{"title": "Book", "author": ""},
{"title": 123, "author": "Jane"},
{"title": "x" * 121, "author": "Jane"},
{"title": "Only title"},
]:
with self.subTest(payload=payload):
self.assertEqual(self.send_json("post", "/books", payload).status_code, 400)
self.assertEqual(
self.client.post("/books", data="not json", content_type="application/json").status_code,
400,
)

def test_missing_book(self):
self.assertEqual(self.client.get("/books/9").status_code, 404)
self.assertEqual(
self.send_json("put", "/books/9", {"title": "X", "author": "Y"}).status_code,
404,
)
self.assertEqual(self.client.delete("/books/9").status_code, 404)

def test_method_not_allowed(self):
self.assertEqual(self.client.post("/books/3").status_code, 405)
Loading
Loading