Kerangka dari Bagian 1 hanya bisa mengirim kembali list kosong. Bagian ini mengisi jantung API: menerima tugas baru lewat POST, memvalidasi isinya, mengembalikannya dalam bentuk yang konsisten, lalu menyediakan GET per id dan DELETE. Alat utamanya adalah dua model Pydantic yang memisahkan apa yang boleh dikirim klien dari apa yang boleh diterima klien — konsep chapter Request Models dan Validation dan Response Models yang kini kita terapkan dalam proyek nyata.

Struktur proyek bertambah satu berkas, sedangkan app/__init__.py tetap kosong seperti sebelumnya:

text
api-manajemen-tugas/
├── pyproject.toml
└── app/
    ├── __init__.py
    ├── main.py
    └── models.py

Model untuk Request dan Response #

Buat berkas app/models.py:

python
from pydantic import BaseModel, Field


class TugasBuat(BaseModel):
    judul: str = Field(min_length=1, examples=["Belajar Python"])


class TugasTampil(BaseModel):
    id: int
    judul: str
    selesai: bool

Mari kita bedah dua class tersebut:

  1. class TugasBuat: kontrak masukan untuk POST /tugas. Field judul wajib berupa string minimal satu karakter — Field(min_length=1) membuat Pydantic menolak judul kosong sebelum fungsi rute sempat jalan.
  2. class TugasTampil: kontrak keluaran. Field id dan selesai tidak ada di TugasBuat karena klien tidak berhak memilihnya — server yang menetapkan keduanya.
  3. Pemisahan dua model ini disengaja: bila masukan dan keluaran dilebur menjadi satu model, klien bisa mengirim id palsu atau selesai: true saat membuat tugas.

Endpoint Utuh dengan Penyimpanan In-Memory #

Ganti seluruh isi app/main.py:

python
from fastapi import FastAPI, status

from app.models import TugasBuat, TugasTampil

app = FastAPI(title="API Manajemen Tugas", version="0.2.0")

tugas_store: list[dict] = []


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/tugas", response_model=list[TugasTampil])
def daftar_tugas() -> list[dict]:
    return tugas_store


@app.post("/tugas", response_model=TugasTampil, status_code=status.HTTP_201_CREATED)
def buat_tugas(payload: TugasBuat) -> dict:
    tugas_baru = {
        "id": len(tugas_store) + 1,
        "judul": payload.judul,
        "selesai": False,
    }
    tugas_store.append(tugas_baru)
    return tugas_baru


@app.get("/tugas/{tugas_id}", response_model=TugasTampil)
def ambil_tugas(tugas_id: int) -> dict:
    for tugas in tugas_store:
        if tugas["id"] == tugas_id:
            return tugas
    raise LookupError(f"tugas dengan id {tugas_id} tidak ditemukan")


@app.delete("/tugas/{tugas_id}", status_code=status.HTTP_204_NO_CONTENT)
def hapus_tugas(tugas_id: int) -> None:
    for indeks, tugas in enumerate(tugas_store):
        if tugas["id"] == tugas_id:
            del tugas_store[indeks]
            return
    raise LookupError(f"tugas dengan id {tugas_id} tidak ditemukan")

Beberapa baris layak diperhatikan lebih dekat:

  1. payload: TugasBuat: FastAPI mengubah body JSON menjadi objek TugasBuat dan menolak body yang tidak lolos validasi — tidak ada parsing manual request.json().
  2. status_code=status.HTTP_201_CREATED: POST yang berhasil menciptakan resource seharusnya menjawab 201, bukan 200 — aturan dari chapter Status Codes dan Error Responses.
  3. "id": len(tugas_store) + 1: server menetapkan id berurutan dan "selesai": False — tugas baru selalu belum selesai.
  4. tugas_id: int pada path: path parameter dikonversi ke int; path seperti /tugas/abc ditolak 422 secara otomatis.
  5. raise LookupError(...): untuk id yang tidak ada, versi ini melempar exception biasa — kita akan lihat akibatnya di akhir bagian, lalu memperbaikinya di Bagian 4.

Uji alurnya dengan skrip sementara cek_api.py:

python
from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)

respons = client.post("/tugas", json={"judul": "Belajar Python"})
print(respons.status_code, respons.json())

respons = client.post("/tugas", json={"judul": "Belajar REST API"})
print(respons.status_code, respons.json())

respons = client.get("/tugas")
print(respons.status_code, respons.json())

Output yang Dihasilkan:

text
201 {'id': 1, 'judul': 'Belajar Python', 'selesai': False}
201 {'id': 2, 'judul': 'Belajar REST API', 'selesai': False}
200 [{'id': 1, 'judul': 'Belajar Python', 'selesai': False}, {'id': 2, 'judul': 'Belajar REST API', 'selesai': False}]

Menyaring Keluaran dengan response_model #

Parameter response_model bukan sekadar dokumentasi: FastAPI memvalidasi keluaran lalu menyingkirkan field yang tidak ada di model. Buktinya, mari sisipkan satu field internal ke penyimpanan, lalu panggil GET /tugas:

python
from fastapi.testclient import TestClient

from app.main import app, tugas_store

client = TestClient(app)

tugas_store.append(
    {
        "id": 1,
        "judul": "Tugas percobaan",
        "selesai": False,
        "catatan_internal": "nilai-yang-tidak-boleh-bocor",
    }
)

respons = client.get("/tugas")
print(respons.status_code, respons.json())

Output yang Dihasilkan:

text
200 [{'id': 1, 'judul': 'Tugas percobaan', 'selesai': False}]

Field catatan_internal tidak pernah sampai ke klien — response_model bertindak sebagai filter keluaran terakhir, sehingga kebocoran field internal tidak mungkin terjadi.

Mengambil dan Menghapus Satu Tugas #

python
from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app)

client.post("/tugas", json={"judul": "Belajar Python"})

respons = client.get("/tugas/1")
print(respons.status_code, respons.json())

respons = client.delete("/tugas/1")
print(respons.status_code, repr(respons.text))

respons = client.get("/tugas")
print(respons.status_code, respons.json())

Output yang Dihasilkan:

text
200 {'id': 1, 'judul': 'Belajar Python', 'selesai': False}
204 ''
200 []

Penghapusan sukses menjawab 204 No Content — status yang benar untuk operasi tanpa isi balasan, sehingga respons.text memang string kosong.

Batas Versi Ini #

Apa yang terjadi bila klien meminta id yang tidak ada? Skrip berikut memeriksanya; perhatikan parameter raise_server_exceptions=False yang membuat TestClient menampilkan respons apa adanya alih-alih meneruskan exception ke skrip kita:

python
from fastapi.testclient import TestClient

from app.main import app

client = TestClient(app, raise_server_exceptions=False)

client.post("/tugas", json={"judul": "Belajar Python"})

respons = client.get("/tugas/99")
print(respons.status_code, repr(respons.text))

Output yang Dihasilkan:

text
500 'Internal Server Error'

Rekap status versi ini:

EndpointMetodeSuksesId tidak ada
/tugasGET200—
/tugasPOST201—
/tugas/{id}GET200500 (sementara)
/tugas/{id}DELETE204500 (sementara)

API kini menerima, menyajikan, dan menghapus tugas — tetapi tugas_store hilang begitu proses berakhir. Kode lengkap tahap ini tersedia pada bagian Berkas Sumber di bawah halaman. Bagian 3 memindahkan penyimpanan ke SQLite agar data bertahan.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

app/main.py

Unduh berkas
python
from fastapi import FastAPI, status

from app.models import TugasBuat, TugasTampil

app = FastAPI(title="API Manajemen Tugas", version="0.2.0")

tugas_store: list[dict] = []


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


@app.get("/tugas", response_model=list[TugasTampil])
def daftar_tugas() -> list[dict]:
    return tugas_store


@app.post("/tugas", response_model=TugasTampil, status_code=status.HTTP_201_CREATED)
def buat_tugas(payload: TugasBuat) -> dict:
    tugas_baru = {
        "id": len(tugas_store) + 1,
        "judul": payload.judul,
        "selesai": False,
    }
    tugas_store.append(tugas_baru)
    return tugas_baru


@app.get("/tugas/{tugas_id}", response_model=TugasTampil)
def ambil_tugas(tugas_id: int) -> dict:
    for tugas in tugas_store:
        if tugas["id"] == tugas_id:
            return tugas
    raise LookupError(f"tugas dengan id {tugas_id} tidak ditemukan")


@app.delete("/tugas/{tugas_id}", status_code=status.HTTP_204_NO_CONTENT)
def hapus_tugas(tugas_id: int) -> None:
    for indeks, tugas in enumerate(tugas_store):
        if tugas["id"] == tugas_id:
            del tugas_store[indeks]
            return
    raise LookupError(f"tugas dengan id {tugas_id} tidak ditemukan")

app/models.py

Unduh berkas
python
from pydantic import BaseModel, Field


class TugasBuat(BaseModel):
    judul: str = Field(min_length=1, examples=["Belajar Python"])


class TugasTampil(BaseModel):
    id: int
    judul: str
    selesai: bool