Episode 16 membangun fitur real-time dengan subscriptions: konsep subscription versus polling, transport WebSocket dengan graphql-ws, setup subscriptions di Apollo Server 4, PubSub pattern dengan Redis, keamanan subscription per koneksi, hingga integrasi client dengan useSubscription.

Sejauh ini semua operasi GraphQL bersifat request-response: client bertanya, server menjawab, selesai. Episode 16 memperkenalkan mode ketiga: subscriptions, di mana server "mendorong" data ke client ketika suatu peristiwa terjadi — tanpa client harus bertanya berulang kali. Kita akan membahas konsep subscription, transport WebSocket dengan library graphql-ws, setup subscriptions di Apollo Server 4, pola PubSub termasuk implementasi Redis untuk production, keamanan per koneksi, dan integrasi client dengan Apollo Client.
Subscription adalah operasi GraphQL yang mempertahankan koneksi aktif. Setiap kali peristiwa yang dipantau terjadi, server mengirim payload terbaru. Use case klasik: chat real-time, notifikasi, dan live updates pada dashboard.
Perbandingan dengan alternatifnya:
Subscriptions umumnya berjalan di atas WebSocket. Library standar modern adalah graphql-ws, yang menggantikan subscriptions-transport-ws yang sudah deprecated. graphql-ws mendukung koneksi yang lebih robust, termasuk penanganan ping/pong dan error recovery; install dengan npm install graphql-ws ws.
Ada juga alternatif Server-Sent Events (SSE) yang satu arah (server ke client) dan berjalan di HTTP biasa — lebih sederhana untuk notifikasi satu arah, tanpa WebSocket. Untuk chat dan kolaborasi dua arah, WebSocket tetap pilihan utama.
Apollo Server 4 menangani subscriptions dengan dua cara: lewat plugin ApolloServerPluginSubscriptionCallback (HTTP callback) atau lewat WebSocket standalone dengan graphql-ws. Pendekatan WebSocket:
import { useServer } from "graphql-ws/lib/use/ws";
import { WebSocketServer } from "ws";
import { createServer } from "http";
const httpServer = createServer();
const wsServer = new WebSocketServer({ server: httpServer, path: "/graphql" });
useServer(
{ schema: server.schema, context: async (ctx) => ({ user: authenticate(ctx) }) },
wsServer
);
httpServer.listen(4001, () => console.log("WS di port 4001"));Subscriptions diekspos di endpoint WebSocket terpisah (/graphql), sementara query dan mutation tetap berjalan di HTTP. Schema-nya:
type Subscription {
messageAdded(roomId: ID!): Message!
notificationReceived: Notification!
}Resolver subscription memiliki dua bagian: subscribe (mengembalikan iterator peristiwa) dan resolve (membentuk payload):
Subscription: {
messageAdded: {
subscribe: (_, args, ctx) =>
ctx.pubsub.asyncIterator(["MESSAGE_ADDED", args.roomId].join(":")),
resolve: (payload) => payload.message,
},
},PubSub (publish-subscribe) memisahkan penerbit peristiwa dari pelanggan. Mutation mempublikasikan peristiwa, subscription menangkapnya:
import { PubSub } from "graphql-subscriptions";
const pubsub = new PubSub();
async function addMessage(_, args, ctx) {
const message = await ctx.db.messages.create(args.input);
await pubsub.publish("MESSAGE_ADDED:general", { message });
return message;
}In-memory PubSub bagus untuk development, tapi tidak bekerja lintas instance server. Jika kalian menjalankan banyak instance (episode 34), peristiwa yang dipublish di instance A tidak akan sampai ke client yang terhubung ke instance B. Solusinya adalah PubSub yang didukung broker bersama.
Redis PubSub menjadi standar untuk subscriptions terdistribusi; install dengan npm install graphql-redis-subscriptions ioredis:
import { RedisPubSub } from "graphql-redis-subscriptions";
import Redis from "ioredis";
const pubsub = new RedisPubSub({
publisher: new Redis(process.env.REDIS_URL),
subscriber: new Redis(process.env.REDIS_URL),
});Semua instance server mendengarkan saluran Redis yang sama, sehingga peristiwa terdistribusi ke semua client di semua instance. Redis PubSub akan kembali dibahas saat scaling WebSocket di episode 34.
Koneksi WebSocket bertahan lama, jadi validasi identitas dilakukan sekali saat koneksi didirikan — lewat connectionParams yang dikirim client:
useServer(
{
schema,
onConnect: (ctx) => {
const token = ctx.connectionParams?.token;
if (!token) throw new Error("Autentikasi gagal");
ctx.user = verifyToken(token);
},
context: (ctx) => ({ user: ctx.user }),
},
wsServer
);Selain autentikasi, terapkan juga authorization pada data subscription (episode 14): pastikan user hanya menerima peristiwa yang berhak mereka lihat, misalnya dengan memfilter topik berdasarkan room membership.
Di sisi client, Apollo Client menyediakan hook useSubscription:
import { useSubscription, gql } from "@apollo/client";
const MESSAGE_ADDED = gql`
subscription OnMessageAdded($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
content
author { username }
}
}
`;
function ChatRoom({ roomId }) {
const { data, loading, error } = useSubscription(MESSAGE_ADDED, {
variables: { roomId },
});
if (loading) return <p>Menghubungkan...</p>;
return <p>{data?.messageAdded.content}</p>;
}Hook ini menangani seluruh lifecycle: menghubungkan koneksi, mengirim variabel, menerima payload, dan memulihkan koneksi saat putus. Setup WebSocket client-nya akan lengkap dibahas di episode 25.
Inti yang harus dibawa pulang:
graphql-ws di atas WebSocket adalah transport modern untuk subscriptions.subscribe (iterator peristiwa) dan resolve (payload).onConnect untuk keamanan per koneksi.useSubscription Apollo Client menangani lifecycle koneksi secara otomatis.Di episode 17 selanjutnya kalian akan mempelajari file upload handling — spesifikasi GraphQL Upload dengan multipart request, implementasi graphql-upload, pemrosesan stream dengan validasi tipe dan ukuran, integrasi penyimpanan seperti AWS S3 dan Cloudinary, hingga keamanan upload file. Media dari user akan bisa masuk ke API kalian!