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:
api-manajemen-tugas/
├── pyproject.toml
└── app/
├── __init__.py
├── main.py
└── models.pyModel untuk Request dan Response #
Buat berkas app/models.py:
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: boolMari kita bedah dua class tersebut:
class TugasBuat: kontrak masukan untukPOST /tugas. Fieldjudulwajib berupa string minimal satu karakter —Field(min_length=1)membuat Pydantic menolak judul kosong sebelum fungsi rute sempat jalan.class TugasTampil: kontrak keluaran. Fieldiddanselesaitidak ada diTugasBuatkarena klien tidak berhak memilihnya — server yang menetapkan keduanya.- Pemisahan dua model ini disengaja: bila masukan dan keluaran dilebur menjadi satu model, klien bisa mengirim
idpalsu atauselesai: truesaat membuat tugas.
Endpoint Utuh dengan Penyimpanan In-Memory #
Ganti seluruh isi app/main.py:
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:
payload: TugasBuat: FastAPI mengubah body JSON menjadi objekTugasBuatdan menolak body yang tidak lolos validasi — tidak ada parsing manualrequest.json().status_code=status.HTTP_201_CREATED:POSTyang berhasil menciptakan resource seharusnya menjawab201, bukan200— aturan dari chapter Status Codes dan Error Responses."id": len(tugas_store) + 1: server menetapkan id berurutan dan"selesai": False— tugas baru selalu belum selesai.tugas_id: intpada path: path parameter dikonversi keint; path seperti/tugas/abcditolak422secara otomatis.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:
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:
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:
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:
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 #
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:
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:
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:
500 'Internal Server Error'Rekap status versi ini:
| Endpoint | Metode | Sukses | Id tidak ada |
|---|---|---|---|
/tugas | GET | 200 | — |
/tugas | POST | 201 | — |
/tugas/{id} | GET | 200 | 500 (sementara) |
/tugas/{id} | DELETE | 204 | 500 (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/__init__.py
Unduh berkasapp/main.py
Unduh berkasfrom 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 berkasfrom 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