Pintu masuk sudah dijaga kontrak request; kini sisi keluar yang harus dikunci. Data internal aplikasi hampir selalu lebih kaya daripada yang boleh dilihat klien: baris pengguna menyimpan password, baris tugas menyimpan catatan internal, dan field seperti itu tidak boleh bocor lewat response JSON. Mengandalkan kehati-hatian saat menyusun dictionary secara manual itu rapuh — satu field lupa dihapus, dan data sensitif terkirim ke seluruh dunia. FastAPI menyediakan pertahanan deklaratif untuk ini: parameter response_model pada path operation menyaring output sehingga hanya field yang dideklarasikan yang lolos ke klien.

Memfilter Field dengan response_model #

Perhatikan API pendaftaran pengguna berikut. Penyimpanan internal (pengguna_db) memang berisi field password, tetapi response JSON tidak boleh memuatnya:

python
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel


class PenggunaCreate(BaseModel):
    nama: str
    email: str
    password: str


class PenggunaResponse(BaseModel):
    id: int
    nama: str
    email: str


app = FastAPI()

pengguna_db = [
    {"id": 1, "nama": "Budi", "email": "[email protected]", "password": "sandi-budi"},
    {"id": 2, "nama": "Siti", "email": "[email protected]", "password": "sandi-siti"},
]


@app.post("/pengguna", response_model=PenggunaResponse, status_code=201)
def daftar_pengguna(pengguna: PenggunaCreate):
    return {"id": 7, **pengguna.model_dump()}


@app.get("/pengguna", response_model=list[PenggunaResponse])
def daftar_semua_pengguna():
    return pengguna_db


client = TestClient(app)

r = client.post(
    "/pengguna",
    json={"nama": "Budi", "email": "[email protected]", "password": "sandi-rahasia"},
)
print(r.status_code, r.json())

print(client.get("/pengguna").json())

Output yang Dihasilkan:

text
201 {'id': 7, 'nama': 'Budi', 'email': '[email protected]'}
[{'id': 1, 'nama': 'Budi', 'email': '[email protected]'}, {'id': 2, 'nama': 'Siti', 'email': '[email protected]'}]

Mari kita telaah mekanismenya:

  1. Baris 12 (class PenggunaResponse(BaseModel)): model response hanya memuat id, nama, dan email — field password sengaja tidak dideklarasikan.
  2. Baris 26 (response_model=PenggunaResponse): sebelum response dikirim, FastAPI melewatkan nilai kembalian fungsi melalui model ini; setiap field di luar deklarasi dibuang.
  3. Baris 28 (return {"id": 7, **pengguna.model_dump()}): dictionary yang dikembalikan oleh fungsi masih memuat password — penyaringan baru terjadi setelahnya, tepat sebelum serialisasi JSON. Output pertama membuktikannya: hanya tiga field yang sampai ke klien.
  4. Baris 31 (response_model=list[PenggunaResponse]): untuk koleksi, gunakan list[...] agar setiap elemen disaring dengan model yang sama — output kedua memperlihatkan dua pengguna tanpa satu pun password.

Memisahkan Model Input dan Output #

Contoh di atas memakai dua model untuk resource yang sama, dan pemisahan ini disengaja:

ModelArahIsiContoh
PenggunaCreateklien → serverfield yang dikirim klien, termasuk passwordrequest body POST
PenggunaResponseserver → klienfield yang aman dilihat, tanpa passwordresponse JSON

Model input dan output punya konsumen dan kebutuhan yang berbeda, jadi menggabungkannya dalam satu class hanya menggandakan risiko: class gabungan yang dipakai sebagai response otomatis mengizinkan klien membaca kembali field rahasia. Dengan dua model, menambah field internal cukup dilakukan pada model input tanpa menyentuh kontrak response.

Bonus Parameter status_code #

Perhatikan parameter status_code=201 pada decorator POST di atas: nilai kembalian fungsi tetap diproses response_model, sementara baris status response berubah dari default 200 OK menjadi 201 Created — sesuai semantik yang kita tetapkan di chapter Desain REST API. Parameter ini juga berlaku untuk method lain, misalnya status_code=204 pada DELETE yang akan kita praktikkan di chapter Status Codes dan Error Responses.