Relasi di API — API

Menampilkan data dari beberapa tabel yang berelasi di API response. Nested resources: GET /users/5/posts → Post milik user 5 GET /posts/3/comments → Komentar…

Buku punya penulis, buku punya review. Data yang saling terhubung seperti ini disebut relasi, dan API harus memutuskan cara mengirimnya: penulisnya ditaruh di dalam buku, atau client mengambilnya sendiri dari alamat lain? Bayangin pesan makanan lewat telepon. Cara pertama: restoran cuma menyebut nomor menu, dan kamu harus menelepon lagi untuk tiap nomor supaya tahu namanya. Cara kedua: mereka langsung membacakan semuanya, lengkap dengan bahan dan cara masaknya, padahal kamu cuma butuh nama. Cara ketiga: kamu bilang di awal "sebutkan namanya juga ya", dan cuma dapat yang kamu minta.

🧪 Tebakan cepat: halaman daftar Toko Buku menampilkan 8 buku, masing-masing dengan nama penulisnya. API cuma mengirim author_id, jadi aplikasi memanggil GET /authors/:id untuk tiap buku. Berapa request totalnya: 2, 5, atau 9? Tebak dulu, jawabannya ada di bagian "N+1 di level API".

Tiga cara mengirim relasi

Data yang dipakai: 8 buku dari pelajaran sebelumnya, 4 penulis, dan 6 review. Tiap review milik satu buku (book_id).

Cara GET /books/1 menjawab Penulisnya
Link (id saja) {"data": {"id": 1, "title": "Laskar Pelangi", "author_id": 1, ...}} client ambil sendiri di GET /authors/1
Embed (disematkan) {"data": {..., "author": {"id": 1, "name": "Andrea Hirata"}}} selalu ikut
Include opsional sama dengan embed, tapi cuma kalau diminta lewat ?include=author ikut kalau diminta

Nested resource: data milik induknya

Review selalu milik satu buku, jadi alamatnya ditulis di bawah alamat bukunya: GET /books/1/reviews artinya "review milik buku 1". Ini disebut nested resource. Contoh lain: komentar milik post 3 ada di GET /api/posts/3/comments.

app.get("/books/:id/reviews", (req, res) => {
  const bookId = Number(req.params.id);
  if (!books.some((b) => b.id === bookId)) {
    return res.status(404).json({ error: "Book not found" });
  }
  const data = reviews.filter((r) => r.book_id === bookId);
  res.json({ data, total: data.length });
});

Cukup satu tingkat: koleksi/id/koleksi. GET /api/posts/3/comments/7/likes/12/users itu nesting terlalu dalam: client harus tahu post, komentar, dan like-nya sekaligus, padahal like 12 sudah punya id sendiri. Pecah jadi endpoint terpisah, misalnya GET /api/likes/12/users.

Embed: penulis di dalam buku

app.get("/books/:id", (req, res) => {
  const book = books.find((b) => b.id === Number(req.params.id));
  if (!book) return res.status(404).json({ error: "Book not found" });
  const author = authors.find((a) => a.id === book.author_id);
  res.json({
    data: {
      id: book.id,
      title: book.title,
      price: book.price,
      author: { id: author.id, name: author.name },
    },
  });
});

Include opsional dengan ?include=

Embed yang selalu ikut juga ada harganya. Halaman daftar butuh nama penulis, tapi gak butuh review. Kalau server selalu menyematkan penulis dan review, body daftar 8 buku jadi 1.528 byte, padahal dengan penulis saja cukup 932 byte. Di server yang memakai database, tiap relasi yang disematkan juga berarti query tambahan. Jadi alasan relasi di-include secara opsional adalah performa: client cuma membayar yang dia minta.

const INCLUDABLE = ["author", "reviews"];

app.get("/books", (req, res) => {
  const include = req.query.include ? req.query.include.split(",") : [];
  if (include.some((name) => !INCLUDABLE.includes(name))) {
    return res.status(400).json({ error: "include must be one of: author, reviews" });
  }
  const data = books.map((book) => {
    const item = { ...book };
    if (include.includes("author")) item.author = authors.find((a) => a.id === book.author_id);
    if (include.includes("reviews")) item.reviews = reviews.filter((r) => r.book_id === book.id);
    return item;
  });
  res.json({ data });
});

N+1 di level API

Kalau API cuma memberi author_id, halaman daftar menulis loop seperti ini:

const { data: books } = await (await fetch("/books")).json();
for (const book of books) {
  const { data: author } = await (await fetch(`/authors/${book.author_id}`)).json();
  book.author = author;
}

Satu request untuk daftar, lalu satu request lagi untuk tiap buku: 1 + 8 = 9 request. Itu jawaban tebakannya. Ini masalah N+1 yang sama dengan di pelajaran Query Database, cuma sekarang terjadi antara client dan API. Kalau satu bolak-balik di jaringan HP butuh 100 milidetik, loop di atas menunggu sekitar 900 milidetik. Mengingat penulis yang sudah diambil memangkasnya jadi 1 + 4 = 5, karena cuma ada 4 penulis. Dengan GET /books?include=author, cukup 1 request.

Cara, halaman daftar 8 buku Request Body yang diterima
Link, loop per buku 9 924 byte
Embed penulis dan review 1 1.528 byte
?include=author 1 932 byte

Di sisi server, include=author juga jangan dikerjakan dengan query per buku. Ambil semua penulisnya dalam satu query (WHERE id IN (...) atau JOIN), seperti di pelajaran N+1 Problem & Eager Loading di track Database.

📌 Di Laravel: API Resource

Di Laravel (pelajaran API Resource di track Laravel), bentuk JSON-nya ditulis di sebuah Resource. $this->kelas membaca relasi kelas milik model:

class SiswaResource extends JsonResource
{
    public function toArray(Request $request): array
    {
        return [
            'id' => $this->id,
            'nama' => $this->nama,
            'kelas' => new KelasResource($this->whenLoaded('kelas')),
        ];
    }
}

⚠️ Jebakan yang sering ketemu

TL;DR: Relasi bisa dikirim tiga cara: link (id saja, client mengambil sendiri), embed (selalu disematkan), atau include opsional (?include=author,reviews, disematkan kalau diminta). Data yang selalu milik induknya pakai nested resource satu tingkat, misalnya /books/1/reviews atau /api/posts/3/comments; /posts/3/comments/7/likes/12/users itu nesting terlalu dalam. Embed memilih field-nya sendiri (author: { id, name }). Include dibuat opsional karena performa: tiap relasi menambah byte dan query, jadi client cuma membayar yang dia minta. Link tanpa include memaksa client ke N+1 di level API: 8 buku = 9 request, dengan include cukup 1. Di Laravel, $this->kelas membaca relasinya, dan $this->whenLoaded('kelas') menyematkannya hanya kalau sudah dimuat dengan with('kelas').

Yang akan kamu pelajari