Baris status adalah bagian pertama yang dibaca mesin dari setiap response — kode 201 memberi tahu klien bahwa resource baru tercipta, sedangkan 200 hanya bilang “sukses” tanpa detail. Kebiasaan lama mengembalikan 200 untuk segala hal lalu menaruh status di dalam body ("{"sukses": false}") memaksa klien mem-parsing body dulu baru tahu request-nya gagal atau tidak — mahal dan rawan salah. Prinsip chapter ini sederhana: status line diisi kode yang benar, dan error mengikuti satu bentuk konsisten. Kita tetap bekerja pada API manajemen tugas dengan endpoint yang semakin lengkap.
Keluarga Status Code #
Lima keluarga status code membagi tanggung jawab yang jelas:
| Keluarga | Makna | Kode yang sering dipakai API |
|---|---|---|
2xx | sukses | 200 OK, 201 Created, 204 No Content |
3xx | redirect / cache | 301 Moved Permanently, 304 Not Modified |
4xx | kesalahan klien | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 422 Unprocessable Entity |
5xx | kesalahan server | 500 Internal Server Error, 503 Service Unavailable |
Aturan praktis untuk CRUD pada API tugas: 200 untuk GET dan PUT yang mengembalikan data, 201 untuk POST yang membuat resource, 204 untuk DELETE yang berhasil, 404 untuk resource yang tidak ada, dan 422 untuk body yang gagal validasi Pydantic (sudah kita lihat di chapter Request Models dan Validation).
Status Sukses dan HTTPException untuk Error #
Parameter status_code pada decorator mengatur kode sukses, sedangkan HTTPException menangani error yang bisa diduga saat runtime:
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel
class TugasBaru(BaseModel):
judul: str
app = FastAPI()
tugas_db = {1: {"id": 1, "judul": "Belajar Python", "selesai": False}}
@app.get("/tugas/{task_id}")
def detail_tugas(task_id: int):
if task_id not in tugas_db:
raise HTTPException(status_code=404, detail=f"Tugas {task_id} tidak ditemukan")
return tugas_db[task_id]
@app.post("/tugas", status_code=201)
def buat_tugas(tugas: TugasBaru):
if any(t["judul"] == tugas.judul for t in tugas_db.values()):
raise HTTPException(status_code=400, detail=f"Judul '{tugas.judul}' sudah dipakai")
id_baru = max(tugas_db) + 1
tugas_db[id_baru] = {"id": id_baru, "judul": tugas.judul, "selesai": False}
return tugas_db[id_baru]
@app.delete("/tugas/{task_id}", status_code=204)
def hapus_tugas(task_id: int):
if task_id not in tugas_db:
raise HTTPException(status_code=404, detail=f"Tugas {task_id} tidak ditemukan")
del tugas_db[task_id]
client = TestClient(app)
print(client.get("/tugas/1").status_code, client.get("/tugas/1").json())
r = client.get("/tugas/99")
print(r.status_code, r.json())
r = client.post("/tugas", json={"judul": "Belajar Python"})
print(r.status_code, r.json())
r = client.post("/tugas", json={"judul": "Belajar status code"})
print(r.status_code, r.json())
r = client.delete("/tugas/1")
print(r.status_code, repr(r.text))
r = client.delete("/tugas/1")
print(r.status_code, r.json())Output yang Dihasilkan:
200 {'id': 1, 'judul': 'Belajar Python', 'selesai': False}
404 {'detail': 'Tugas 99 tidak ditemukan'}
400 {'detail': "Judul 'Belajar Python' sudah dipakai"}
201 {'id': 2, 'judul': 'Belajar status code', 'selesai': False}
204 ''
404 {'detail': 'Tugas 1 tidak ditemukan'}Mari kita telaah baris-baris kuncinya:
- Baris 17 (
raise HTTPException(status_code=404, ...)):raisemenghentikan fungsi saat itu juga; FastAPI menangkap exception ini dan mengirim response404dengan body{"detail": ...}— tidak perlu menulis try/except di kode rute. - Baris 21 (
@app.post("/tugas", status_code=201)): POST sukses mengembalikan201 Created, bukan200— resource baru saja tercipta dan body masih memuat data tugasnya. - Baris 23-24 (
raise HTTPException(status_code=400, ...)):400 Bad Requestdipakai untuk request yang sah secara struktur tetapi melanggar aturan bisnis — di sini judul duplikat. Bedakan dengan422: yang satu ditolak aturan domain kita, yang lain gagal validasi Pydantic. - Baris 30 (
status_code=204):204 No Contentberarti “sukses tanpa ada yang perlu dikirim kembali” — perhatikan output'': body benar-benar kosong, bukan"null". - Baris 49-52 (dua panggilan DELETE): pemanggilan pertama mengembalikan
204, pemanggilan kedua404karena tugas sudah tiada. DELETE tetap idempotent — efeknya sama (tugas 1 tidak ada) — meskipun kode statusnya berbeda.
Custom Exception Handler untuk Konsistensi #
HTTPException menghasilkan body berbentuk {"detail": ...}. Bentuk ini sah, tetapi beberapa tim ingin format error yang lebih kaya atau seragam untuk seluruh domain — misalnya kode error internal tugas_tidak_ditemukan. FastAPI memungkinkan kita mendefinisikan exception sendiri beserta handler-nya:
from fastapi import Request
from fastapi.responses import JSONResponse
class TugasTidakDitemukan(Exception):
def __init__(self, task_id: int):
self.task_id = task_id
@app.exception_handler(TugasTidakDitemukan)
async def handler_tugas_tidak_ditemukan(request: Request, exc: TugasTidakDitemukan):
return JSONResponse(
status_code=404,
content={"error": "tugas_tidak_ditemukan", "task_id": exc.task_id},
)
@app.get("/laporan/tugas/{task_id}")
def laporan_tugas(task_id: int):
if task_id not in tugas_db:
raise TugasTidakDitemukan(task_id)
return {"task_id": task_id, "status": "aktif"}
print(client.get("/laporan/tugas/99").status_code,
client.get("/laporan/tugas/99").json())Output yang Dihasilkan:
404 {'error': 'tugas_tidak_ditemukan', 'task_id': 99}Handler terdaftar lewat decorator @app.exception_handler(TugasTidakDitemukan); FastAPI memanggil handler tersebut setiap kali exception itu terbangkitkan dari rute mana pun, lalu JSONResponse menentukan status code dan body secara penuh. Fungsi handler ditulis async def mengikuti konvensi FastAPI untuk exception handler — materi async/await dibahas pada Section Asynchronous Programming.
Dengan sukses dan error beres, endpoint berikutnya yang perlu kita rapikan adalah daftar tugas yang terus membesar — uraian pagination ada di chapter Pagination, Filtering, dan Sorting.