Setiap frontend yang menampilkan banyak data punya masalah yang sama: endpoint REST mengembalikan lebih banyak field daripada yang dibutuhkan, atau malah kurang sehingga harus memanggil beberapa endpoint sekaligus. GraphQL hadir untuk memecahkan masalah ini dengan membiarkan client menentukan sendiri bentuk data yang diinginkan.

Artikel ini membahas GraphQL secara praktis: cara kerjanya, implementasi server dengan Apollo, schema design, resolver, dan masalah N+1 yang paling sering menjatuhkan orang.

Masalah yang Dipecahkan GraphQL

Bayangkan halaman profil yang butuh tiga data: profil user, daftar artikel user, dan jumlah follower. Dengan REST, ini biasanya berarti tiga request terpisah:

GET /api/users/42          → profil
GET /api/users/42/articles → daftar artikel
GET /api/users/42/followers → jumlah follower

Dengan GraphQL, cukup satu query:

query {
  user(id: "42") {
    name
    email
    articles {
      title
      publishedAt
    }
    followerCount
  }
}

Server mengembalikan persis field yang diminta — tidak lebih, tidak kurang. Inilah dua manfaat inti GraphQL: menghindari over-fetching (data berlebihan) dan under-fetching (harus banyak request).

Schema: Kontrak yang Menjadi Dokumentasi

Inti GraphQL adalah schema yang strongly typed. Schema mendefinisikan tipe dan hubungannya, dan otomatis menjadi dokumentasi serta alat validasi query.

Contoh schema sederhana:

type User {
  id: ID!
  name: String!
  email: String!
  articles: [Article!]!
  followerCount: Int!
}

type Article {
  id: ID!
  title: String!
  content: String!
  publishedAt: String
  author: User!
}

type Query {
  user(id: ID!): User
  articles(limit: Int): [Article!]!
}

type Mutation {
  createArticle(title: String!, content: String!): Article!
}

Tanda seru ! berarti non-null, [Article!]! berarti list yang tidak boleh null dan isinya juga tidak boleh null. Typing ini memungkinkan validasi query di compile time sebelum menyentuh server.

Implementasi Server dengan Apollo

Apollo Server adalah implementasi GraphQL paling populer di ekosistem Node.js. Berikut setup lengkapnya:

import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    articles: [Article!]!
  }

  type Article {
    id: ID!
    title: String!
    content: String!
    authorId: ID!
  }

  type Query {
    user(id: ID!): User
    articles: [Article!]!
  }

  type Mutation {
    createArticle(title: String!, content: String!, authorId: ID!): Article!
  }
`;

const db = {
  users: [
    { id: "1", name: "Afry", email: "afry@example.com" },
    { id: "2", name: "Bima", email: "bima@example.com" },
  ],
  articles: [
    { id: "a1", title: "Belajar GraphQL", content: "Konten...", authorId: "1" },
    { id: "a2", title: "REST vs GraphQL", content: "Konten...", authorId: "1" },
  ],
};

const resolvers = {
  Query: {
    user: (_, { id }) => db.users.find((u) => u.id === id),
    articles: () => db.articles,
  },
  User: {
    articles: (parent) => db.articles.filter((a) => a.authorId === parent.id),
  },
  Mutation: {
    createArticle: (_, { title, content, authorId }) => {
      const article = { id: String(db.articles.length + 1), title, content, authorId };
      db.articles.push(article);
      return article;
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { port: 4000 });
console.log(`Server ready at ${url}`);

Struktur typeDefs + resolvers adalah pola dasar yang harus dipahami. typeDefs mendefinisikan bentuk data, resolvers mendefinisikan cara mengambil data.

Resolver: Field-Level Resolution

Hal penting yang sering terlewat: di GraphQL, setiap field punya resolver-nya sendiri. Parent resolver mengembalikan object, lalu GraphQL memanggil resolver untuk setiap field yang diminta.

const resolvers = {
  Query: {
    user: (parent, args, context) => db.users.find((u) => u.id === args.id),
  },
  // Resolver untuk field `articles` di tipe User
  User: {
    articles: (parent) => db.articles.filter((a) => a.authorId === parent.id),
  },
};

Argumen yang diterima resolver:

  • parent — hasil resolver di level atas
  • args — argumen query
  • context — object bersama (auth, db client, dll)
  • info — metadata query

Ini memberi fleksibilitas luar biasa: field articles bisa di-resolve dari database, cache, atau API eksternal tanpa mengubah schema.

Masalah N+1 dan Solusinya

Ini adalah jebakan paling umum dalam implementasi GraphQL. Ketika query meminta user beserta articles untuk banyak user:

query {
  articles {
    title
    author {
      name
    }
  }
}

Tanpa batching, resolver author akan dipanggil sekali per article. Kalau ada 100 article, server melakukan 101 query database — satu untuk article, 100 untuk author. Inilah masalah N+1.

Solusinya: DataLoader, library batching dari Facebook. DataLoader mengumpulkan semua permintaan di satu "tick" dan mengeksekusinya sebagai satu query batch.

import DataLoader from "dataloader";

const authorLoader = new DataLoader(async (ids) => {
  const authors = await db.findAuthorsByIds(ids);
  // Harus dikembalikan sesuai urutan ids
  return ids.map((id) => authors.find((a) => a.id === id));
});

const resolvers = {
  Article: {
    author: (parent) => authorLoader.load(parent.authorId),
  },
};

Sekarang 100 pemanggilan authorLoader.load() di-merge menjadi satu query SELECT * FROM authors WHERE id IN (...). DataLoader wajib dipahami sebelum GraphQL masuk produksi.

Mutation dan Error Handling

Mutation adalah operasi tulis. Berbeda dengan Query, mutation dieksekusi secara serial — urutannya dijamin.

mutation {
  createArticle(title: "Judul", content: "Isi") {
    id
    title
  }
}

Untuk error, GraphQL punya dua mekanisme:

  1. Field error — error yang muncul di array errors response, sementara data lain tetap valid.
  2. Union type — untuk error yang perlu dibedakan oleh client, gunakan union:
type CreateArticleResult {
  article: Article
  error: CreateArticleError
}

type CreateArticleError {
  code: String!
  message: String!
}

type Mutation {
  createArticle(title: String!, content: String!): CreateArticleResult!
}

Pendekatan union memungkinkan client menangani sukses dan gagal secara terstruktur, bukan parsing string error.

Integrasi dengan REST dan Database

GraphQL tidak menggantikan database atau REST API internal. Resolver bisa memanggil REST endpoint, database, gRPC, atau apa pun:

const resolvers = {
  User: {
    // Field ini di-resolve dari REST API eksternal
    followerCount: async (parent) => {
      const res = await fetch(`https://api.external.com/users/${parent.id}/followers/count`);
      return res.json();
    },
  },
};

GraphQL adalah lapisan agregasi di atas sumber data yang sudah ada, bukan pengganti semuanya.

Keamanan GraphQL

GraphQL punya tantangan keamanan khusus:

  1. Depth limiting — query bersarang dalam bisa menyebabkan DoS. Batasi kedalaman query.
  2. Query complexity analysis — hitung "cost" query dan tolak yang terlalu mahal.
  3. Rate limiting — GraphQL membuat satu request bisa melakukan banyak pekerjaan, jadi rate limiting harus lebih ketat.
  4. Authorization per field — karena client bisa meminta field apa pun, otorisasi harus ditegakkan di level resolver, bukan level endpoint.

Contoh depth limiting sederhana:

import depthLimit from "graphql-depth-limit";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(7)],
});

Jangan pernah mengandalkan "menyembunyikan field dari dokumentasi" sebagai keamanan — schema selalu bisa di-introspect.

Kapan Memilih GraphQL

GraphQL bukan pengganti universal untuk REST. Gunakan GraphQL ketika:

  • Client punya kebutuhan query yang sangat dinamis
  • Banyak client berbeda (web, mobile, dll) dengan kebutuhan data berbeda
  • Data punya banyak relasi kompleks
  • Tim ingin mengurangi round-trip API

Tetap pakai REST ketika:

  • API sederhana dengan sedikit resource
  • Kebutuhan caching CDN sederhana (GraphQL menyulitkan HTTP caching)
  • Client sedikit dan bentuk data stabil
  • Upload file besar (GraphQL kurang ideal untuk ini)

GraphQL punya trade-off nyata: caching HTTP jadi lebih sulit, kompleksitas server naik, dan N+1 harus diatasi dengan sengaja.

FAQ

Apakah GraphQL lebih cepat dari REST? Tidak otomatis. GraphQL mengurangi round-trip dan over-fetching, tapi menambah overhead query parsing dan resolver. Kecepatan tergantung implementasi — N+1 yang tidak ditangani justru membuat GraphQL lebih lambat.

Apakah GraphQL cocok untuk upload file? Kurang ideal. GraphQL berbasis JSON, sementara upload file butuh multipart. Ada spesifikasi GraphQL multipart, tapi REST/object storage biasanya lebih sederhana.

Bagaimana dengan caching? GraphQL mempersulit HTTP caching karena semua query lewat satu endpoint dengan POST. Solusinya: persisted queries, client-side cache (Apollo Client), atau caching di level resolver dengan Redis.

Apakah saya perlu migrasi penuh dari REST? Tidak. Banyak tim menjalankan GraphQL sebagai lapisan di atas REST yang sudah ada, atau migrasi bertahap per resource. Keduanya bisa hidup berdampingan.

Kesimpulan Praktis

GraphQL memecahkan masalah over-fetching dan under-fetching dengan memberi client kontrol penuh atas bentuk data. Tapi kekuatan ini datang dengan tanggung jawab: schema design yang matang, resolver yang efisien, dan penanganan N+1 yang wajib.

Tiga hal yang harus dikuasai sebelum produksi: schema yang strongly typed, pola resolver per field, dan DataLoader untuk batching. Tanpa DataLoader, GraphQL yang terlihat cepat di development akan menjadi lambat di produksi.

GraphQL adalah alat yang tepat untuk API dengan client dinamis dan data relasional kompleks. Untuk API sederhana, REST yang terdesain baik tetap merupakan pilihan yang valid dan lebih mudah dioperasikan.