Membuat Aplikasi Real-time dengan GraphQL Subscriptions: Panduan Lengkap
1. Pendahuluan
Pernahkah Anda menggunakan aplikasi chat, melihat notifikasi instan, atau memantau harga saham yang diperbarui secara langsung tanpa perlu me-refresh halaman? Di balik pengalaman pengguna yang mulus ini, ada teknologi yang memungkinkan data mengalir secara real-time dari server ke klien. Salah satu cara modern dan elegan untuk mencapainya dalam ekosistem API adalah dengan GraphQL Subscriptions.
Dunia web modern menuntut interaktivitas dan responsivitas yang tinggi. Pola komunikasi tradisional seperti polling (klien berulang kali meminta update ke server) seringkali tidak efisien, membuang resource server, dan menghasilkan latency yang tidak diinginkan. Bayangkan jika aplikasi chat Anda harus menanyakan “Ada pesan baru?” setiap beberapa detik – sungguh boros dan lambat!
Di sinilah GraphQL Subscriptions hadir sebagai solusi. Alih-alih klien yang terus bertanya, server justru yang proaktif “memberi tahu” klien ketika ada data baru yang tersedia. Ini memungkinkan pengalaman pengguna yang jauh lebih baik, efisiensi resource yang lebih tinggi, dan arsitektur aplikasi yang lebih tangguh untuk fitur-fitur real-time.
Dalam artikel ini, kita akan menyelami GraphQL Subscriptions: apa itu, bagaimana cara kerjanya, kapan harus menggunakannya, dan bagaimana mengimplementasikannya dalam aplikasi Anda. Mari kita mulai!
2. Apa Itu GraphQL Subscriptions dan Bagaimana Cara Kerjanya?
GraphQL Subscriptions adalah salah satu dari tiga jenis operasi di GraphQL, selain Query (untuk mengambil data) dan Mutation (untuk mengubah data). Fungsi utamanya adalah untuk mendengarkan perubahan data secara real-time.
💡 Analogi Sederhana: Bayangkan Anda ingin tahu kapan tukang pos mengantarkan surat baru.
- Query seperti Anda pergi ke kantor pos setiap hari untuk menanyakan “Apakah ada surat untuk saya hari ini?”.
- Mutation seperti Anda mengirim surat baru melalui kantor pos.
- Subscription seperti Anda berlangganan layanan notifikasi. Kantor pos akan menelepon Anda secara otomatis saat ada surat baru yang datang untuk Anda, tanpa Anda harus bertanya.
Mekanisme di Balik Layar: WebSocket
GraphQL Subscriptions biasanya diimplementasikan di atas protokol WebSocket. Ini adalah protokol komunikasi dua arah yang persisten antara klien dan server. Berbeda dengan HTTP yang bersifat request-response satu kali, WebSocket menjaga koneksi tetap terbuka, memungkinkan server untuk “mendorong” data kapan saja ke klien yang telah berlangganan.
📌 Alur Kerja Umum:
- Klien Berlangganan: Klien mengirimkan operasi
subscriptionke server GraphQL melalui koneksi WebSocket. Dalam operasi ini, klien menyatakan data apa yang ingin didengarkan. - Server Menerima Langganan: Server memvalidasi langganan dan mendaftarkan klien sebagai “pelanggan” untuk jenis event tertentu.
- Event Terjadi: Ketika ada perubahan data yang relevan (misalnya, ada komentar baru di postingan), server akan memicu sebuah event.
- Server Menerbitkan Data: Server “menerbitkan” data perubahan tersebut ke semua klien yang telah berlangganan event tersebut.
- Klien Menerima Data: Klien yang berlangganan menerima data update secara instan melalui koneksi WebSocket yang terbuka.
Konsep ini dikenal sebagai Publish/Subscribe (Pub/Sub) Pattern. Server adalah “publisher” yang menerbitkan event, dan klien adalah “subscriber” yang berlangganan event tersebut.
3. Perbedaan dengan Query dan Mutation
Penting untuk memahami kapan harus menggunakan Query, Mutation, dan Subscription.
| Fitur | Query | Mutation | Subscription |
|---|---|---|---|
| Tujuan | Mengambil data | Mengubah data (create, update, delete) | Mendengarkan perubahan data real-time |
| Sifat | Idempotent (biasanya) | Non-idempotent (biasanya) | Non-idempotent (memicu event) |
| Komunikasi | Request-response (HTTP) | Request-response (HTTP) | Persistent connection (WebSocket) |
| Pemicu | Klien meminta data | Klien mengirim perubahan data | Server mengirim data ketika ada event |
| Contoh | Mengambil daftar postingan, detail user | Membuat postingan baru, update profil | Notifikasi komentar baru, update harga saham |
❌ Kapan Tidak Menggunakan Subscription:
- Untuk data yang hanya perlu diambil sekali (gunakan
Query). - Untuk data yang berubah sangat jarang dan polling sederhana sudah cukup (misal, update status server setiap 5 menit).
- Untuk operasi yang hanya mengubah data tanpa perlu notifikasi real-time ke klien lain (gunakan
Mutation).
✅ Kapan Menggunakan Subscription:
- Aplikasi chat
- Notifikasi instan (misal, notifikasi email baru, notifikasi like di media sosial)
- Live dashboard atau analytics
- Pelacakan lokasi real-time
- Update harga saham atau skor pertandingan live
- Kolaborasi dokumen secara real-time
4. Membangun GraphQL Subscription: Contoh Praktis
Mari kita lihat bagaimana mengimplementasikan GraphQL Subscriptions dengan contoh sederhana: notifikasi komentar baru. Kita akan menggunakan apollo-server untuk backend dan apollo-client untuk frontend.
4.1. Server-Side (Node.js dengan Apollo Server)
Pertama, kita perlu mendefinisikan skema GraphQL untuk subscription kita.
# typeDefs.js
const typeDefs = `
type Comment {
id: ID!
content: String!
author: String!
postId: ID!
}
type Query {
comments(postId: ID!): [Comment!]!
}
type Mutation {
addComment(content: String!, author: String!, postId: ID!): Comment!
}
type Subscription {
# Ketika ada komentar baru, klien akan menerima objek Comment
commentAdded(postId: ID!): Comment!
}
`;
module.exports = typeDefs;
Selanjutnya, kita perlu mengimplementasikan resolver untuk subscription ini. Kita akan menggunakan graphql-subscriptions dan PubSub-nya untuk mekanisme Pub/Sub sederhana.
// resolvers.js
const { PubSub } = require("graphql-subscriptions");
const pubsub = new PubSub();
// Nama event yang akan kita gunakan untuk menerbitkan dan berlangganan
const COMMENT_ADDED = "COMMENT_ADDED";
// Data dummy
const comments = [];
let commentId = 0;
const resolvers = {
Query: {
comments: (_, { postId }) => comments.filter((c) => c.postId === postId),
},
Mutation: {
addComment: (_, { content, author, postId }) => {
const newComment = { id: String(++commentId), content, author, postId };
comments.push(newComment);
// 🎯 Menerbitkan event ketika ada komentar baru
// Parameter kedua adalah payload yang akan dikirim ke subscriber
// Parameter ketiga adalah filter untuk subscription (opsional, tapi sangat berguna)
pubsub.publish(COMMENT_ADDED, { commentAdded: newComment, postId });
return newComment;
},
},
Subscription: {
commentAdded: {
// 📌 Resolver untuk subscription. `subscribe` adalah fungsi yang mengembalikan AsyncIterator.
// Apollo Server akan secara otomatis memfilter berdasarkan postId yang diberikan oleh klien.
subscribe: (_, { postId }) => {
// Menggunakan AsyncIterator untuk mendengarkan event COMMENT_ADDED
// dan memfilter hanya untuk postId yang relevan
return pubsub.asyncIterator([COMMENT_ADDED], {
filter: (payload, variables) => {
return payload.postId === variables.postId;
},
});
},
// ✅ Resolver untuk data yang diterima.
// Ini akan mengambil nilai `commentAdded` dari payload yang diterbitkan.
resolve: (payload) => {
return payload.commentAdded;
},
},
},
};
module.exports = resolvers;
Terakhir, siapkan Apollo Server untuk mendukung WebSocket.
// index.js
const { ApolloServer } = require("apollo-server");
const { PubSub } = require("graphql-subscriptions"); // Pastikan Anda menggunakan PubSub yang sama
const typeDefs = require("./typeDefs");
const resolvers = require("./resolvers");
const pubsub = new PubSub(); // Inisialisasi PubSub
const server = new ApolloServer({
typeDefs,
resolvers,
// Context untuk menyediakan pubsub ke resolvers
context: ({ req, connection }) => {
if (connection) {
// Untuk WebSocket connection (subscriptions)
return { ...connection.context, pubsub };
}
// Untuk HTTP request (queries dan mutations)
return { ...req, pubsub };
},
// Mengaktifkan subscriptions
subscriptions: {
onConnect: (connectionParams, websocket, context) => {
console.log("Client connected for subscriptions!");
// Anda bisa melakukan autentikasi di sini
// if (!connectionParams.authToken) {
// throw new Error('Missing auth token!');
// }
// return { currentUser: getUser(connectionParams.authToken) };
},
onDisconnect: (websocket, context) => {
console.log("Client disconnected from subscriptions.");
},
},
});
server.listen().then(({ url, subscriptionsUrl }) => {
console.log(`🚀 Server ready at ${url}`);
console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`);
});
4.2. Client-Side (React dengan Apollo Client)
Di sisi klien (misalnya, aplikasi React), kita perlu mengkonfigurasi Apollo Client untuk menggunakan koneksi WebSocket.
// App.js atau index.js di aplikasi React Anda
import React from "react";
import ReactDOM from "react-dom";
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
useQuery,
useMutation,
useSubscription,
gql,
} from "@apollo/client";
import { WebSocketLink } from "@apollo/client/link/ws";
import { split, HttpLink } from "@apollo/client";
import { getMainDefinition } from "@apollo/client/utilities";
// Konfigurasi link HTTP untuk Query dan Mutation
const httpLink = new HttpLink({
uri: "http://localhost:4000/graphql",
});
// Konfigurasi link WebSocket untuk Subscription
const wsLink = new WebSocketLink({
uri: "ws://localhost:4000/graphql", // Gunakan ws:// atau wss://
options: {
reconnect: true,
// connectionParams: {
// authToken: localStorage.getItem('token'),
// },
},
});
// Menggunakan split untuk merutekan operasi ke link yang tepat
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
// --- Komponen React ---
// GraphQL Queries, Mutations, Subscriptions
const GET_COMMENTS = gql`
query GetComments($postId: ID!) {
comments(postId: $postId) {
id
content
author
}
}
`;
const ADD_COMMENT = gql`
mutation AddComment($content: String!, $author: String!, $postId: ID!) {
addComment(content: $content, author: $author, postId: $postId) {
id
content
author
}
}
`;
const COMMENT_ADDED_SUBSCRIPTION = gql`
subscription CommentAdded($postId: ID!) {
commentAdded(postId: $postId) {
id
content
author
}
}
`;
function CommentSection({ postId }) {
const { loading, error, data } = useQuery(GET_COMMENTS, {
variables: { postId },
});
const [addComment] = useMutation(ADD_COMMENT, {
refetchQueries: [{ query: GET_COMMENTS, variables: { postId } }], // Opsional: refetch setelah mutation
});
// 🎯 Menggunakan useSubscription untuk mendengarkan komentar baru
useSubscription(COMMENT_ADDED_SUBSCRIPTION, {
variables: { postId },
onSubscriptionData: ({ client, subscriptionData }) => {
// ✅ Ketika ada data baru dari subscription, update cache Apollo
const newComment = subscriptionData.data.commentAdded;
client.cache.updateQuery(
{ query: GET_COMMENTS, variables: { postId } },
(data) => {
if (data) {
return {
comments: [...data.comments, newComment],
};
}
return data;
},
);
console.log("Komentar baru diterima:", newComment);
},
});
const handleSubmit = async (e) => {
e.preventDefault();
const content = e.target.content.value;
const author = e.target.author.value;
if (content && author) {
await addComment({ variables: { content, author, postId } });
e.target.content.value = "";
}
};
if (loading) return <p>Memuat komentar...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
<h2>Komentar untuk Post ID: {postId}</h2>
{data.comments.map((comment) => (
<div
key={comment.id}
style={{
border: "1px solid #eee",
padding: "10px",
margin: "10px 0",
}}
>
<strong>{comment.author}:</strong> {comment.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input type="text" name="author" placeholder="Nama Anda" required />
<input
type="text"
name="content"
placeholder="Tulis komentar..."
required
/>
<button type="submit">Kirim Komentar</button>
</form>
</div>
);
}
function App() {
return (
<ApolloProvider client={client}>
<h1>Aplikasi Notifikasi Komentar Real-time</h1>
<CommentSection postId="1" />
<hr />
<CommentSection postId="2" /> {/* Contoh untuk post ID berbeda */}
</ApolloProvider>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
Dengan konfigurasi ini, setiap kali ada komentar baru ditambahkan (melalui mutation), server akan menerbitkan event, dan semua klien yang berlangganan commentAdded untuk postId yang sama akan menerima komentar baru secara instan. Fungsi onSubscriptionData pada useSubscription kemudian akan memperbarui cache Apollo Client secara manual agar UI otomatis ter-render ulang.
5. Pertimbangan dan Best Practices
Meskipun PubSub dari graphql-subscriptions sangat bagus untuk memulai, ia memiliki keterbatasan, terutama untuk aplikasi terdistribusi atau skala besar.
5.1. Skalabilitas
PubSub bawaan graphql-subscriptions hanya bekerja dalam satu instance aplikasi. Jika Anda memiliki beberapa server GraphQL (misalnya, di lingkungan microservices atau load-balanced), event yang diterbitkan oleh satu server tidak akan diketahui oleh server lain.
⚠️ Solusi Skalabilitas:
Untuk mengatasi ini, Anda bisa mengintegrasikan PubSub dengan external message broker seperti:
- Redis Pub/Sub: Pilihan populer karena Redis cepat dan mendukung fitur Pub/Sub. Anda bisa menggunakan
graphql-redis-subscriptions. - Apache Kafka: Untuk event streaming skala besar dan durabilitas tinggi.
- Google Cloud Pub/Sub, AWS SNS/SQS, Azure Service Bus: Layanan message queue berbasis cloud yang terkelola.
5.2. Autentikasi dan Otorisasi
Sama seperti Query dan Mutation, Subscriptions juga perlu diamankan.
- Autentikasi: Saat koneksi WebSocket dibuat (
onConnect), Anda bisa memeriksa token autentikasi (misalnya, JWT) yang dikirim oleh klien melaluiconnectionParams. - Otorisasi: Dalam resolver
subscribeatauresolveAnda, Anda bisa memeriksa peran atau izin pengguna sebelum mengizinkan mereka untuk berlangganan atau menerima data.
5.3. Penanganan Kesalahan (Error Handling)
Pastikan untuk menangani kesalahan baik di sisi server maupun klien. Di server, resolver subscribe harus bisa menangani error dan meneruskannya. Di klien, useSubscription memiliki parameter onError untuk menangani error yang diterima.
5.4. Pengelolaan Koneksi
Koneksi WebSocket bersifat persisten. Pastikan klien dapat otomatis mencoba menyambung ulang (reconnect) jika koneksi terputus (Apollo Client WebSocketLink sudah mendukung ini secara default). Server juga harus dapat membersihkan langganan ketika klien terputus.
5.5. Keamanan
- DDoS Attack: Koneksi WebSocket yang persisten bisa menjadi target serangan DDoS. Implementasikan rate limiting pada koneksi WebSocket atau gunakan reverse proxy yang kuat seperti Nginx.
- Payload Size: Batasi ukuran payload event yang diterbitkan untuk mencegah resource exhaustion.
Kesimpulan
GraphQL Subscriptions adalah alat yang sangat kuat untuk membangun aplikasi web real-time yang modern dan responsif. Dengan memanfaatkan protokol WebSocket dan pola Pub/Sub, Anda dapat memberikan pengalaman pengguna yang lebih dinamis dan efisien dibandingkan dengan teknik polling tradisional.
Meskipun implementasi dasar cukup mudah dengan Apollo Server dan Client, penting untuk mempertimbangkan aspek skalabilitas, keamanan, dan penanganan kesalahan saat membangun aplikasi produksi. Dengan pemahaman yang baik tentang konsep dan best practices, Anda akan siap untuk mengintegrasikan fitur real-time yang mengagumkan ke dalam proyek GraphQL Anda.
Selamat mencoba dan berkreasi dengan GraphQL Subscriptions!