Rekan ArtonLabs, kalau Anda sedang memilih pustaka untuk API Django berikutnya, semoga tulisan ini mempersingkat pertimbangannya.
Django REST Framework adalah pilihan aman yang sudah lama, tetapi untuk API baru kami makin sering memilih Django Ninja: skema berbasis Pydantic, anotasi tipe Python biasa, dokumentasi OpenAPI yang muncul sendiri, dan kode yang jauh lebih pendek. Aplikasi riset yang kami kelola memakainya untuk memulai pekerjaan analisis dan memantau progresnya. Tulisan ini membangun API kecil dari awal, termasuk autentikasi token dan beberapa jebakan versi yang pernah menjatuhkan situs kami.
Daftar Isi
- Instalasi dan Endpoint Pertama
- Skema Masukan dan Keluaran
- Operasi Dasar dengan Model Django
- Autentikasi: Sesi dan Token
- CSRF dan Jebakan Versi 1.x
- Dokumentasi Otomatis
- Menguji
- Penutup
Instalasi dan Endpoint Pertama
pip install django-ninja
# api.py
from ninja import NinjaAPI
api = NinjaAPI(title="API Proyek", version="1.0")
@api.get("/halo")
def halo(request, nama: str = "dunia"):
return {"pesan": f"halo, {nama}"}
# urls.py
from django.urls import path
from .api import api
urlpatterns = [path("api/", api.urls)]
Parameter fungsi dengan anotasi tipe otomatis menjadi parameter query yang divalidasi. Buka /api/docs dan Anda sudah punya halaman dokumentasi interaktif.
Skema Masukan dan Keluaran
from ninja import Schema
from datetime import datetime
class PencarianIn(Schema):
kata_kunci: str
sumber: list[str]
batas: int = 100
class PencarianOut(Schema):
id: int
kata_kunci: str
status: str
dibuat: datetime
Skema adalah model Pydantic. Masukan yang tidak cocok ditolak dengan respons 422 yang menjelaskan bidang mana yang salah, tanpa satu baris validasi manual. Skema keluaran membatasi bidang yang dikembalikan, jadi bidang internal tidak bocor hanya karena ada di model.
Operasi Dasar dengan Model Django
from django.shortcuts import get_object_or_404
from .models import Pencarian
@api.post("/pencarian", response=PencarianOut)
def buat(request, data: PencarianIn):
return Pencarian.objects.create(owner=request.user, **data.dict())
@api.get("/pencarian/{id}", response=PencarianOut)
def detail(request, id: int):
return get_object_or_404(Pencarian, pk=id, owner=request.user)
@api.get("/pencarian", response=list[PencarianOut])
def daftar(request):
return Pencarian.objects.filter(owner=request.user)
Perhatikan owner=request.user di setiap query. Kami pernah menemukan endpoint status yang lupa menyaring pemilik, sehingga siapa pun yang menebak id bisa membaca progres analisis orang lain. Saring kepemilikan di query, bukan setelahnya.
Autentikasi: Sesi dan Token
Untuk klien di browser yang sudah login, pakai sesi Django. Untuk skrip dan integrasi, token Bearer:
from ninja.security import HttpBearer, django_auth
from .models import ApiToken
class TokenAuth(HttpBearer):
def authenticate(self, request, token):
t = ApiToken.objects.filter(key=token, aktif=True).select_related("user").first()
if t:
request.user = t.user
return t.user
api = NinjaAPI(auth=[django_auth, TokenAuth()])
Daftar auth dicoba berurutan: sesi dulu, lalu token. Endpoint publik bisa membebaskan diri dengan auth=None. Simpan token sebagai hash, bukan teks polos, dan beri pengguna cara mencabutnya. Ini pola yang sama dengan yang dipakai REST API VinzaPanel.
CSRF dan Jebakan Versi 1.x
Ini yang pernah menjatuhkan situs kami sampai 500. Di Django Ninja 0.x, Anda menulis NinjaAPI(csrf=True). Di versi 1.x, argumen itu dihapus; menulisnya memunculkan TypeError saat impor, dan seluruh situs ikut mati. Cara yang benar di 1.x: pasang django_auth pada endpoint yang memakai sesi, karena autentikasi sesi itu sendiri yang mengaktifkan pemeriksaan CSRF. Klien di browser lalu mengirim header X-CSRFToken dari cookie; kami membungkus fetch di templat dasar agar header itu selalu ikut pada permintaan non-GET. Permintaan dengan token Bearer tidak butuh CSRF, karena tidak bergantung pada cookie.
Mengunggah Berkas
from ninja import File, Form
from ninja.files import UploadedFile
@api.post("/pencarian/{id}/lampiran")
def unggah(request, id: int, berkas: UploadedFile = File(...), keterangan: str = Form("")):
p = get_object_or_404(Pencarian, pk=id, owner=request.user)
if berkas.size > 10 * 1024 * 1024:
return api.create_response(request, {"detail": "maksimal 10 MB"}, status=413)
p.lampiran.save(berkas.name, berkas)
return {"ukuran": berkas.size, "nama": berkas.name}
Batasi ukuran dan periksa jenis isi berkas, bukan hanya ekstensinya. Simpan dengan nama yang dibuat server, bukan nama dari pengguna, supaya tidak ada jalur yang bisa dimanipulasi.
Versi API
Saat bentuk respons harus berubah, buat instance kedua NinjaAPI(version="2.0", urls_namespace="v2") dan pasang di /api/v2/. Klien lama tetap memakai v1 sampai siap pindah. Ninja mengharuskan namespace yang berbeda untuk tiap instance; tanpa itu ia menolak saat mulai, dan itu pengingat yang baik.
Dokumentasi Otomatis
Swagger UI di /api/docs dan skema di /api/openapi.json dihasilkan dari kode. Tambahkan summary dan description pada dekorator agar dokumentasinya terbaca manusia. Satu catatan: halaman docs bawaan memuat skrip dari CDN, dan skemanya menerbitkan daftar lengkap endpoint kepada siapa pun. Di produksi kami mematikannya dengan docs_url=None dan openapi_url=None saat DEBUG=False, dan membukanya hanya di lingkungan pengembangan.
Menguji
from django.test import TestCase
class ApiTest(TestCase):
def test_butuh_login(self):
r = self.client.get("/api/pencarian/1")
self.assertEqual(r.status_code, 401)
def test_tidak_bisa_baca_milik_orang(self):
self.client.force_login(self.pengguna_lain)
r = self.client.get(f"/api/pencarian/{self.milik_saya.id}")
self.assertEqual(r.status_code, 404)
Dua uji itu, untuk setiap endpoint, menutup kelas bug yang paling sering kami temui di audit: endpoint tanpa autentikasi dan endpoint yang lupa menyaring pemilik. Ninja juga menyediakan TestClient sendiri untuk menguji API tanpa lapisan URL Django.
Paginasi dan Penyaringan
from ninja.pagination import paginate, PageNumberPagination
@api.get("/pencarian", response=list[PencarianOut])
@paginate(PageNumberPagination, page_size=25)
def daftar(request, status: str | None = None):
qs = Pencarian.objects.filter(owner=request.user)
if status:
qs = qs.filter(status=status)
return qs.order_by("-dibuat")
Dekorator paginate menambahkan parameter page dan membungkus respons dengan jumlah total. Mengembalikan seluruh tabel tanpa paginasi adalah kesalahan yang tidak terasa di pengembangan dan terasa sekali setelah setahun data menumpuk.
Router untuk Memecah Modul
# pencarian/api.py
from ninja import Router
router = Router(tags=["pencarian"])
@router.get("/")
def daftar(request): ...
# api.py
from pencarian.api import router as pencarian_router
api.add_router("/pencarian", pencarian_router)
Setiap aplikasi Django memiliki router sendiri, dan berkas api.py utama hanya menyatukannya. Tag membuat dokumentasi terkelompok rapi.
Menangani Galat Secara Seragam
from django.core.exceptions import PermissionDenied
@api.exception_handler(PermissionDenied)
def ditolak(request, exc):
return api.create_response(request, {"detail": "tidak diizinkan"}, status=403)
Dengan penangan galat terpusat, klien selalu menerima bentuk JSON yang sama, dan Anda bisa mencatat galat di satu tempat. Galat validasi Pydantic sudah ditangani Ninja dengan respons 422 yang rapi; jangan menimpanya kecuali perlu.
daripada API yang salah: tipe memvalidasi masukan, skema membatasi keluaran, dan dokumentasi lahir sendiri. Yang tetap menjadi tanggung jawab Anda adalah autentikasi di setiap endpoint dan penyaringan kepemilikan di setiap query; dua uji kecil per endpoint sudah cukup untuk menjaganya.Terima kasih sudah membaca. Pertanyaan seputar autentikasi dan versi API bisa disampaikan lewat halaman kontak.