1. Pengenalan GraphQL
GraphQL adalah bahasa query dan runtime untuk API yang dikembangkan oleh Meta (Facebook) pada tahun 2012 dan dirilis sebagai open-source pada tahun 2015. GraphQL memungkinkan klien meminta data yang spesifik sesuai kebutuhan — tidak lebih, tidak kurang.
Berbeda dengan REST yang menggunakan banyak endpoint, GraphQL menggunakan satu endpoint untuk semua operasi. Klien mengirim query yang mendeskripsikan data apa yang dibutuhkan, dan server mengembalikan response dengan struktur yang persis sesuai query tersebut.
Konsep Inti GraphQL
| Konsep | Penjelasan |
|---|---|
| Schema | Kontrak antara client dan server yang mendefinisikan tipe data dan operasi yang tersedia |
| Query | Operasi untuk mengambil data (setara dengan GET di REST) |
| Mutation | Operasi untuk mengubah data (setara dengan POST/PUT/DELETE di REST) |
| Subscription | Operasi untuk data realtime via WebSocket |
| Resolver | Fungsi yang menangani setiap field dalam schema — berisi logika bisnis |
| Type System | Sistem tipe yang kuat untuk memvalidasi query secara otomatis |
Mengapa GraphQL?
| Keunggulan | Penjelasan |
|---|---|
| Tidak Ada Over-fetching | Klien hanya meminta data yang dibutuhkan — menghemat bandwidth |
| Tidak Ada Under-fetching | Satu query bisa mengambil data dari beberapa resource sekaligus |
| Strongly Typed | Schema menjamin data yang dikembalikan selalu sesuai kontrak |
| Introspection | Client bisa menanyakan schema untuk mengetahui data apa saja yang tersedia |
| Tooling | GraphQL Playground, Apollo DevTools, dan code generation |
| Ekosistem Besar | Apollo, Relay, urql, GraphQL Code Generator, dan banyak lagi |
┌─────────────────────────────────────────────────────────┐
│ CLIENT │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ GraphQL Query │ │
│ │ │ │
│ │ query { │ │
│ │ user(id: 1) { │ │
│ │ nama │ │
│ │ email │ │
│ │ posts { judul } │ │
│ │ } │ │
│ │ } │ │
│ └───────────────────────┬──────────────────────────┘ │
│ │ HTTP POST │
└──────────────────────────┼──────────────────────────────┘
│
┌──────────────────────────▼──────────────────────────────┐
│ GRAPHQL SERVER │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Schema Definition (SDL) │ │
│ │ → Validasi query terhadap schema │ │
│ └───────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────────┐ │
│ │ Resolvers │ │
│ │ → user resolver → ambil dari database │ │
│ │ → posts resolver → ambil relasi posts │ │
│ └───────────────────────┬───────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────────┐ │
│ │ Response (JSON) │ │
│ │ { "data": { "user": { "nama": "Budi", ... } } } │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
GraphQL bukan pengganti database — ia adalah lapisan API yang berada di atas database (atau sumber data apa pun). GraphQL bisa mengambil data dari database SQL, NoSQL, REST API, microservices, atau bahkan file lokal.
2. GraphQL vs REST
Untuk memahami mengapa GraphQL lahir, kita perlu memahami keterbatasan REST dan bagaimana GraphQL mengatasinya. Mari kita bandungkan keduanya dengan contoh kasus nyata.
Contoh Kasus: Mengambil Data Pengguna dan Post
| Aspek | REST | GraphQL |
|---|---|---|
| Endpoint | GET /api/users/1 | POST /graphql |
| Jumlah Request | 2-3 request (user + posts + comments) | 1 request saja |
| Over-fetching | Mengembalikan semua field user | Hanya field yang diminta |
| Under-fetching | Perlu request terpisah untuk posts | Include posts dalam satu query |
| Dokumentasi | Swagger / OpenAPI (manual) | Introspection (otomatis) |
| Versioning | /api/v1/users, /api/v2/users | Tidak perlu versioning |
REST: Multiple Requests
// REST: Perlu 3 request terpisah untuk mendapatkan semua data
// Request 1: Ambil data user
const user = await fetch('/api/users/1');
// Response: { id: 1, nama: "Budi", email: "budi@mail.com", umur: 25, alamat: "...", ... }
// Request 2: Ambil post user
const posts = await fetch('/api/users/1/posts');
// Response: [{ id: 1, judul: "Post 1", konten: "...", ... }, ...]
// Request 3: Ambil komentar setiap post
const comments = await fetch('/api/posts/1/comments');
// Response: [{ id: 1, teks: "Bagus!", ... }, ...]
// Masalah:
// - Over-fetching: user response punya field yang tidak dibutuhkan
// - Under-fetching: butuh request terpisah untuk posts dan comments
// - Waterfall: request 2 menunggu request 1 selesai
GraphQL: Satu Request
# GraphQL: Semua data dalam satu query
query {
user(id: 1) {
nama
email
posts {
judul
published
comments {
teks
author {
nama
}
}
}
}
}
# Response (JSON):
# {
# "data": {
# "user": {
# "nama": "Budi",
# "email": "budi@mail.com",
# "posts": [
# {
# "judul": "Post Pertama",
# "published": true,
# "comments": [
# {
# "teks": "Artikel bagus!",
# "author": { "nama": "Sari" }
# }
# ]
# }
# ]
# }
# }
# }
GraphQL tidak selalu lebih baik dari REST. Gunakan REST untuk API sederhana dengan struktur data yang tidak berubah, file upload yang kompleks, atau caching HTTP yang kuat. GraphQL ideal untuk API yang digunakan oleh banyak client (web, mobile, IoT) dengan kebutuhan data yang berbeda-beda.
3. Mendefinisikan Schema
Schema adalah jantung dari API GraphQL. Schema mendefinisikan tipe data apa yang tersedia, field apa yang dimiliki setiap tipe, dan operasi (query, mutation, subscription) apa yang bisa dilakukan. Schema ditulis menggunakan Schema Definition Language (SDL).
Scalar Types (Tipe Dasar)
| Tipe | Deskripsi | Contoh |
|---|---|---|
| Int | Integer 32-bit | 25, -10, 0 |
| Float | Desimal floating-point | 3.14, -0.5 |
| String | Teks UTF-8 | "Hello", "Budi" |
| Boolean | Benar atau salah | true, false |
| ID | Identifier unik (serialized sebagai String) | "abc123" |
Object Types dan Schema
# Mendefinisikan tipe data User
type User {
id: ID!
nama: String!
email: String!
umur: Int
isActive: Boolean!
posts: [Post!]!
profile: Profile
createdAt: String!
}
# Tanda ! berarti field wajib (non-nullable)
# [Post!]! berarti array yang tidak null dan elemennya tidak null
type Post {
id: ID!
judul: String!
konten: String
published: Boolean!
author: User!
comments: [Comment!]!
tags: [Tag!]!
createdAt: String!
}
type Comment {
id: ID!
teks: String!
author: User!
post: Post!
}
type Tag {
id: ID!
nama: String!
posts: [Post!]!
}
type Profile {
id: ID!
bio: String
avatar: String
user: User!
}
# Root types — mendefinisikan operasi yang tersedia
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
posts(published: Boolean, tag: String): [Post!]!
searchPosts(keyword: String!): [Post!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
publishPost(id: ID!): Post!
}
type Subscription {
postCreated: Post!
commentAdded(postId: ID!): Comment!
}
Input Types
# Input types digunakan sebagai argumen untuk mutations
# Input type mirip dengan object type, tapi khusus untuk input
input CreateUserInput {
nama: String!
email: String!
umur: Int
}
input UpdateUserInput {
nama: String
email: String
umur: Int
}
input CreatePostInput {
judul: String!
konten: String
authorId: ID!
tagIds: [ID!]
}
# Enum — tipe dengan nilai yang terbatas
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
enum SortOrder {
ASC
DESC
}
# Interface — kontrak yang harus dipenuhi oleh tipe
interface Node {
id: ID!
}
# Union — gabungan beberapa tipe
union SearchResult = User | Post | Comment
Gunakan input types untuk argumen mutation agar schema lebih rapi dan reusable. Jangan mengirim banyak argumen scalar — kemas dalam satu input type. Ini juga memudahkan validasi di sisi server.
4. Queries: Mengambil Data
Query adalah operasi untuk mengambil data dari server GraphQL. Query memiliki struktur yang sama dengan response JSON yang diharapkan — ini memudahkan developer memahami data apa yang akan diterima.
Query Dasar
# Query sederhana
query {
users {
id
nama
email
}
}
# Query dengan argumen
query {
user(id: "1") {
nama
email
umur
profile {
bio
avatar
}
}
}
# Query dengan variabel
query GetUser($userId: ID!) {
user(id: $userId) {
nama
email
posts {
judul
published
}
}
}
# Variabel dikirim terpisah:
# { "userId": "1" }
# Query dengan directive
query GetUser($withPosts: Boolean!) {
user(id: "1") {
nama
email
posts @include(if: $withPosts) {
judul
published
}
}
}
# Fragment — reusable bagian query
fragment UserBasicInfo on User {
id
nama
email
isActive
}
fragment PostDetail on Post {
id
judul
konten
published
createdAt
}
query GetDashboard {
users(limit: 10) {
...UserBasicInfo
}
recentPosts: posts(published: true) {
...PostDetail
author {
nama
}
}
}
# Aliases — mengquery tipe yang sama dengan argumen berbeda
query {
activeUsers: users(limit: 5) {
nama
}
inactiveUsers: users(limit: 5) {
nama
}
}
Nested Queries dan Relasi
# Mengambil data dengan relasi bertingkat
query {
user(id: "1") {
nama
posts {
judul
comments {
teks
author {
nama
profile {
avatar
}
}
}
tags {
nama
}
}
}
}
# Response yang dihasilkan:
# {
# "data": {
# "user": {
# "nama": "Budi",
# "posts": [
# {
# "judul": "Belajar GraphQL",
# "comments": [
# {
# "teks": "Mantap!",
# "author": {
# "nama": "Sari",
# "profile": {
# "avatar": "https://..."
# }
# }
# }
# ],
# "tags": [
# { "nama": "GraphQL" },
# { "nama": "API" }
# ]
# }
# ]
# }
# }
# }
5. Mutations: Mengubah Data
Mutation adalah operasi untuk membuat, memperbarui, atau menghapus data. Berbeda dengan query yang bisa dijalankan secara paralel, mutation dieksekusi secara berurutan untuk menjaga konsistensi data.
# Membuat user baru
mutation {
createUser(input: {
nama: "Budi Santoso"
email: "budi@example.com"
umur: 25
}) {
id
nama
email
createdAt
}
}
# Mengupdate user
mutation {
updateUser(
id: "1"
input: {
nama: "Budi S. Updated"
umur: 26
}
) {
id
nama
umur
}
}
# Menghapus user
mutation {
deleteUser(id: "5")
}
# Mutation dengan variabel
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
judul
author {
nama
}
}
}
# Variabel:
# {
# "input": {
# "judul": "Post Baru",
# "konten": "Ini konten post...",
# "authorId": "1",
# "tagIds": ["1", "2"]
# }
# }
# Multiple mutations dalam satu request
mutation {
post1: createPost(input: { judul: "Post 1", authorId: "1" }) {
id
judul
}
post2: createPost(input: { judul: "Post 2", authorId: "1" }) {
id
judul
}
}
Error Handling di Mutation
# Pola error handling dengan Union type
union CreateUserResult = User | ValidationError
type ValidationError {
field: String!
message: String!
}
mutation {
createUser(input: { nama: "", email: "invalid" }) {
... on User {
id
nama
email
}
... on ValidationError {
field
message
}
}
}
# Response jika ada error:
# {
# "data": {
# "createUser": {
# "__typename": "ValidationError",
# "field": "nama",
# "message": "Nama tidak boleh kosong"
# }
# }
# }
6. Subscriptions: Data Realtime
Subscription memungkinkan client menerima data secara realtime ketika ada perubahan di server. Subscription menggunakan WebSocket sebagai transport protocol, bukan HTTP biasa.
# Schema untuk subscription
type Subscription {
# Triggered ketika post baru dibuat
postCreated: Post!
# Triggered ketika komentar ditambahkan ke post tertentu
commentAdded(postId: ID!): Comment!
# Triggered ketika user online status berubah
userStatusChanged: User!
}
# Client mengirim subscription:
subscription OnNewPost {
postCreated {
id
judul
author {
nama
}
createdAt
}
}
# Client mengirim subscription untuk komentar spesifik:
subscription OnNewComment($postId: ID!) {
commentAdded(postId: $postId) {
id
teks
author {
nama
}
createdAt
}
}
Contoh Implementasi Resolver Subscription
// Menggunakan PubSub untuk mengelola event
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const resolvers = {
Subscription: {
postCreated: {
subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
},
commentAdded: {
subscribe: (_, { postId }) => {
return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
},
},
},
Mutation: {
createPost: async (_, { input }, context) => {
const post = await context.db.post.create({ data: input });
// Publish event ke semua subscriber
pubsub.publish('POST_CREATED', {
postCreated: post,
});
return post;
},
addComment: async (_, { postId, teks }, context) => {
const comment = await context.db.comment.create({
data: { teks, postId, authorId: context.user.id },
});
pubsub.publish(`COMMENT_ADDED_${postId}`, {
commentAdded: comment,
});
return comment;
},
},
};
Subscription membutuhkan WebSocket server yang terpisah dari HTTP server. Di production, pastikan menggunakan library seperti graphql-ws (bukan subscriptions-transport-ws yang sudah deprecated). Perhatikan juga limit koneksi WebSocket agar server tidak overload.
7. Apollo Server: Implementasi
Apollo Server adalah server GraphQL paling populer untuk Node.js. Ia menyediakan semua yang dibutuhkan untuk membangun GraphQL API: schema validation, execution engine, integrasi dengan Express, dan Apollo Sandbox untuk testing.
Instalasi dan Setup
# Instal dependencies npm install @apollo/server graphql # Untuk Express integration npm install @apollo/server express cors body-parser
Membuat Server GraphQL
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// Data dummy (di production, gunakan database)
const users = [
{ id: '1', nama: 'Budi', email: 'budi@mail.com', umur: 25 },
{ id: '2', nama: 'Sari', email: 'sari@mail.com', umur: 28 },
{ id: '3', nama: 'Andi', email: 'andi@mail.com', umur: 30 },
];
const posts = [
{ id: '1', judul: 'Belajar GraphQL', konten: 'GraphQL itu...', authorId: '1', published: true },
{ id: '2', judul: 'Tips API Design', konten: 'API yang baik...', authorId: '1', published: false },
{ id: '3', judul: 'Tutorial Apollo', konten: 'Apollo Server...', authorId: '2', published: true },
];
// Type Definitions (Schema)
const typeDefs = `
type User {
id: ID!
nama: String!
email: String!
umur: Int
posts: [Post!]!
}
type Post {
id: ID!
judul: String!
konten: String
published: Boolean!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts(published: Boolean): [Post!]!
}
type Mutation {
createUser(nama: String!, email: String!, umur: Int): User!
createPost(judul: String!, konten: String, authorId: ID!): Post!
}
`;
// Resolvers
const resolvers = {
Query: {
users: () => users,
user: (_, { id }) => users.find(u => u.id === id),
posts: (_, { published }) => {
if (published !== undefined) {
return posts.filter(p => p.published === published);
}
return posts;
},
},
// Resolver untuk field-level — menangani relasi
User: {
posts: (parent) => posts.filter(p => p.authorId === parent.id),
},
Post: {
author: (parent) => users.find(u => u.id === parent.authorId),
},
Mutation: {
createUser: (_, { nama, email, umur }) => {
const newUser = {
id: String(users.length + 1),
nama,
email,
umur,
};
users.push(newUser);
return newUser;
},
createPost: (_, { judul, konten, authorId }) => {
const newPost = {
id: String(posts.length + 1),
judul,
konten,
authorId,
published: false,
};
posts.push(newPost);
return newPost;
},
},
};
// Buat dan jalankan server
const server = new ApolloServer({
typeDefs,
resolvers,
});
const { url } = await startStandaloneServer(server, {
listen: { port: 4000 },
});
console.log(`🚀 Server GraphQL berjalan di ${url}`);
// Output: 🚀 Server GraphQL berjalan di http://localhost:4000/
Testing di Apollo Sandbox
# Buka http://localhost:4000 di browser untuk Apollo Sandbox
# Test query:
query {
users {
nama
email
posts {
judul
published
}
}
}
# Test mutation:
mutation {
createUser(nama: "Maya", email: "maya@mail.com", umur: 22) {
id
nama
email
}
}
# Test mutation dengan response data:
mutation {
createPost(judul: "Post dari Sandbox", konten: "Testing!", authorId: "1") {
id
judul
author {
nama
}
}
}
Apollo Sandbox otomatis mendeteksi schema Anda dan menyediakan autocomplete, validasi, dan dokumentasi interaktif. Ini sangat membantu saat development karena Anda bisa langsung mencoba query tanpa perlu Postman atau tool API lainnya.
8. Quiz: Uji Pemahamanmu!
Setelah membaca tutorial di atas, jawablah 5 pertanyaan berikut untuk menguji pemahamanmu tentang GraphQL: