Merancang REST API yang konsisten: konvensi resource dan HTTP methods, format JSON request/response, pagination untuk list data, API versioning, dan pola endpoint JSON yang siap dikonsumsi SPA dan mobile client.

Di episode-episode sebelumnya kita membangun endpoint JSON secara sporadis — setiap view memutuskan sendiri formatnya. Episode 14 merangkai semuanya menjadi desain API yang konsisten: konvensi resource, format response standar, pagination, dan versioning. Ini yang membedakan "kumpulan endpoint" dari REST API yang profesional.
Prinsipnya: REST API yang baik bisa dikonsumsi oleh siapa pun (SPA, mobile, integrasi) tanpa membaca source code — karena konvensi dan formatnya seragam.
REST memodelkan data sebagai resource (/users, /posts, /items) yang dimanipulasi dengan HTTP methods. Konvensi standarnya:
| Method | URL | Fungsi | Status sukses |
|---|---|---|---|
| GET | /users | List users | 200 |
| GET | /users/42 | Detail user | 200 |
| POST | /users | Buat user baru | 201 |
| PUT | /users/42 | Update penuh user | 200 |
| PATCH | /users/42 | Update sebagian | 200 |
| DELETE | /users/42 | Hapus user | 200/204 |
Aturan penamaan: resource jamak (/users, bukan /user), URL berisi noun, bukan verb (/users/42, bukan /getUser). Aksi yang tidak cocok dengan CRUD (misal "ban user") memakai sub-resource: POST /users/42/ban atau POST /users/42/bans.
Konsistensi format adalah setengah dari kualitas API. Satu pola response standar:
def api_ok(data, status: int = 200) -> tuple[dict, int]:
return jsonify({"data": data}), status
def api_error(message: str, status: int) -> tuple[dict, int]:
return jsonify({"error": message, "status": status}), statusResponse sukses selalu membungkus payload di data; error selalu konsisten berbentuk {"error": ..., "status": ...} — lanjutan dari episode 5. Contoh endpoint:
@api_bp.get("/users")
def list_users() -> tuple[dict, int]:
users = db.session.scalars(db.select(User).order_by(User.id)).all()
return api_ok([
{"id": u.id, "username": u.username, "email": u.email}
for u in users
])Validasi input POST memakai request.get_json(silent=True) dan pola error 400 dari episode 5, atau bentuk yang lebih kaya lewat form/schema library (episode 12).
Mengembalikan seluruh baris tabel dalam satu response adalah anti-pattern: lambat dan membebani client. Solusinya pagination — potong data per halaman:
@api_bp.get("/users")
def list_users() -> tuple[dict, int]:
page = request.args.get("page", 1, type=int)
per_page = min(request.args.get("per_page", 20, type=int), 100)
pagination = db.paginate(
db.select(User).order_by(User.id), page=page, per_page=per_page
)
return api_ok({
"items": [
{"id": u.id, "username": u.username} for u in pagination.items
],
"page": pagination.page,
"per_page": pagination.per_page,
"total": pagination.total,
"pages": pagination.pages,
})db.paginate() menangani hitung total dan slicing. Selalu batasi per_page (di atas pakai min(..., 100)) agar satu request tidak bisa menarik seluruh tabel. Client berpindah halaman dengan GET /api/users?page=2&per_page=20.
Tip
Untuk API yang lebih canggih, pertimbangkan cursor pagination: halaman berikutnya direferensikan lewat token cursor (misal ?cursor=eyJpZCI6...), bukan nomor halaman. Lebih stabil terhadap data yang bertambah di tengah penelusuran. Nomor halaman sederhana cukup untuk mayoritas aplikasi — jangan over-engineer.
Saat API berevolusi, perubahan format bisa memecah client lama. Versioning memberi ruang bernapas:
/api/v1/users
/api/v2/usersDi Flask, versi adalah blueprint terpisah:
api_v1_bp = Blueprint("api_v1", __name__, url_prefix="/api/v1")
api_v2_bp = Blueprint("api_v2", __name__, url_prefix="/api/v2")
# di factory:
app.register_blueprint(api_v1_bp)
app.register_blueprint(api_v2_bp)Aturan praktis: v1 stabil selamanya; perubahan breaking masuk ke v2; fitur baru non-breaking ditambahkan ke versi aktif. Client yang belum siap migrasi tetap bekerja di v1 selama masa transisi.
REST API sebaiknya dipisah dari halaman web — buat blueprint khusus api dengan error handler sendiri:
api_bp = Blueprint("api", __name__, url_prefix="/api")
@api_bp.app_errorhandler(404)
def api_not_found(e: Exception) -> tuple[dict, int]:
return api_error("resource tidak ditemukan", 404)
@api_bp.app_errorhandler(400)
def api_bad_request(e: Exception) -> tuple[dict, int]:
return api_error("request tidak valid", 400)Dengan handler per blueprint (episode 9), halaman web bisa menampilkan error HTML sementara API selalu mengembalikan JSON.
Beberapa hal yang wajib sejak dini di API:
int tanpa konversi yang aman.flask-cors — jangan * tanpa berpikir./getUser, /deletePost): pakai method + noun resource.{data: ...} dan {items: ...} di endpoint berbeda.Pada episode 14 ini, kalian telah merancang REST API yang konsisten dan siap produksi.
Inti yang harus dibawa pulang:
{"data": ...} sukses, {"error": ..., "status": ...} gagal.db.paginate() untuk list data; batasi per_page.url_prefix (/api/v1, /api/v2).Di episode 15 selanjutnya, kita melindungi dan mempercepat API: caching & rate limiting — Flask-Caching dengan Redis, per-view caching, Flask-Limiter dengan limit per-user/IP, dan kombinasi cache response + proteksi API. Sampai jumpa di episode 15!