L
GraphQL API Development • Literally Web ID

Membangun Real-Time GraphQL API dengan Subscriptions dan Redis Pub/Sub

⏱️ 15 menit baca • 📅 15 Oct 2026 • 👁️ 1 views
Membangun Real-Time GraphQL API dengan Subscriptions dan Redis Pub/Sub

1. Konteks Industri & Analisis Akar Masalah

Dalam lanskap arsitektur web modern, ekspektasi pengguna terhadap interaktivitas sistem telah bergeser dari model Request-Response tradisional (HTTP REST) menuju pengalaman Real-Time instan tanpa page refresh. Fitur seperti live tracking, kolaborasi dokumen secara langsung, pembaruan data pasar saham, dan notifikasi instan telah menjadi standar minimum aplikasi enterprise. Namun, di balik kebutuhan ini tersimpan tantangan teknis yang masif bagi backend engineers dan infrastructure architects.

Secara historis, arsitektur RESTful murni mengalami kegagalan struktural ketika dipaksa menangani kebutuhan real-time. Pendekatan paling primitif yang sering digunakan adalah HTTP Polling (klien secara periodik mengirim permintaan GET ke server setiap beberapa detik). Pendekatan ini menghasilkan overhead network yang masif, membebani CPU database dengan kueri berulang yang sebagian besar menghasilkan data redundan (tidak ada perubahan), serta meningkatkan latensi sistem secara keseluruhan. Pendekatan yang sedikit lebih baik adalah Long Polling, namun teknik ini tetap memakan banyak koneksi persisten (file descriptors) pada web server dan tidak efisien untuk komunikasi dua arah atau broadcast masif.

Kehadiran protokol WebSocket merevolusi cara aplikasi web berkomunikasi dengan membuka saluran komunikasi TCP full-duplex jangka panjang. Di atas ekosistem WebSocket, GraphQL Subscriptions lahir untuk memberikan solusi elegan dalam pengambilan data secara real-time. Dengan GraphQL Subscriptions, klien dapat mendeklarasikan bentuk data spesifik yang ingin mereka terima setiap kali sebuah event terjadi di server. Klien hanya menerima delta data yang relevan, menghemat bandwidth secara drastis, dan mempertahankan fleksibilitas sistem tipe data yang kuat dari GraphQL.

[!WARNING] Masalah kritis muncul ketika aplikasi harus melakukan scaling (penskalaan horizontal) melampaui batas satu node server. Ketika instans aplikasi diduplikasi di belakang sebuah Load Balancer (misalnya Nginx atau Cloudflare), koneksi WebSocket dari klien terdistribusi secara acak ke Node A, Node B, dan Node C. Jika sebuah mutasi data dieksekusi dan memicu subscription trigger di Node A, klien yang terhubung ke Node B tidak akan pernah menerima pembaruan tersebut karena tidak adanya mekanisme komunikasi antar-server (inter-node communication).

Sebelum penerapan arsitektur terdistribusi berbasis Pub/Sub, sistem sering kali mengalami data fragmentation, lost events, lonjakan penggunaan memori lokal yang tidak terkendali pada satu server (memory leak akibat manajemen memori WebSocket mentah), serta kegagalan total saat terjadi lonjakan traffic (traffic spikes). Setelah penerapan arsitektur Real-Time GraphQL API dengan Subscriptions dan Redis Pub/Sub, setiap node server bertindak sebagai subscriber independen terhadap satu message broker terpusat (Redis). Mutasi data di node mana pun akan mempublikasikan pesan ke Redis, yang kemudian secara instan menyiarkannya ke seluruh node server lain, memastikan setiap klien mendapatkan pembaruan secara sinkron dan konsisten terlepas dari node mana mereka terhubung.


2. Konsep Inti & Deep Dive Arsitektur

Untuk memahami secara utuh bagaimana arsitektur ini bekerja di level produksi, kita harus membedah alur hidup (lifecycle) dari sebuah permintaan subscription hingga eksekusi pengiriman data melalui WebSocket dan Redis Pub/Sub.

Alur Eksekusi Data (Data Flow Architecture)

+-----------------------+
|      Web Client       |
+-----------------------+
            |
            | 1. WebSocket Connection & GQL Subscription Request
            v
+-----------------------+
|      Load Balancer    |
+-----------------------+
            |
            | 2. Proxy to Node Server
            v
+-------------------------------------------------------------+
|                        Node Server A                        |
|  +-------------------------------------------------------+  |
|  |                  GraphQL Subscription Engine          |  |
|  +-------------------------------------------------------+  |
|                            |                                |
|                            | 3. Register Listener           |
+----------------------------|--------------------------------+
                             |
                             v
+-------------------------------------------------------------+
|                        Redis Server                         | <--- (Redis Pub/Sub Channel)
+-------------------------------------------------------------+
                             ^
                             |
                             | 4. Publish Event (Mutation on Node B)
+----------------------------|--------------------------------+
|                        Node Server B                        |
|  +-------------------------------------------------------+  |
|  |                    GraphQL Mutation                   |  |
|  +-------------------------------------------------------+  |
+-------------------------------------------------------------+

Penjelasan Komponen Utama

  1. Klien & GraphQL WebSocket Transport (graphql-ws): Klien memulai jabat tangan WebSocket (WebSocket handshake) ke server. Setelah koneksi terbuka, klien mengirimkan pesan connection_init diikuti oleh payload subscribe yang berisi kueri GraphQL Subscription. Server mendaftarkan klien ini ke dalam subscription manager lokal.
  2. Apollo Server & Subscription Server (subscriptions-transport-ws / graphql-ws): Server backend (Node.js/TypeScript dalam hal ini) mengelola pemetaan antara ID koneksi klien dan event triggers.
  3. Redis Pub/Sub Layer: Sebagai tulang punggung arsitektur terdistribusi, Redis menyediakan mekanisme Publish/Subscribe. Saat mutasi terjadi di server manapun, server tersebut memanggil fungsi pubsub.publish('CHANNEL_NAME', payload). Redis menerima pesan ini dan mendistribusikannya ke seluruh instans aplikasi backend yang berlangganan pada channel tersebut.
  4. GraphQL Redis PubSub Engine (graphql-redis-subscriptions): Pustaka perantara yang menjembatani antara sistem event bawaan GraphQL (PubSubEngine) dengan Redis server, menerjemahkan event lokal menjadi pesan jaringan Redis dan sebaliknya.

3. Perbandingan Komparatif / Analisis Pendekatan

Sebelum memilih pendekatan Redis Pub/Sub untuk arsitektur GraphQL Subscriptions, mari kita evaluasi berbagai pilihan arsitektur yang tersedia dalam industri rekayasa perangkat lunak.

Parameter Perbandingan In-Memory PubSub (Default) Redis Pub/Sub Apache Kafka / RabbitMQ
Kompleksitas Setup Sangat Rendah (Out of box) Rendah - Sedang Tinggi
Skalabilitas Multi-Node Tidak Bisa (Isolated) Sangat Tinggi Sangat Tinggi
Persistensi Pesan Tidak Ada (Volatile) Tidak Ada (Volatile)* Ada (Durable Log)
Konsumsi Sumber Daya Sangat Rendah (RAM lokal) Rendah (RAM) Tinggi (CPU, RAM, Disk)
Latensi Jaringan Mendekati 0 ms (In-proc) Sangat Rendah (< 2ms) Sedang - Tinggi
Kasus Penggunaan Aplikasi single-instance Real-time API, Chat, Dashboard Event-Driven Microservices, Data Pipeline

**Catatan: Redis juga memiliki fitur Redis Streams untuk persistensi, namun untuk kebutuhan Pub/Sub murni, pesan bersifat ephemeral (hilang jika tidak ada subscriber aktif saat itu).*-

[!TIP] Gunakan In-Memory PubSub hanya untuk lingkungan pengembangan lokal (development) atau aplikasi skala kecil yang berjalan pada satu instans server tunggal. Untuk produksi dengan auto-scaling group di balik load balancer, Redis Pub/Sub adalah standar industri yang paling efisien dan mudah dikelola.


4. Prasyarat Sistem & Persiapan Lingkungan

Sebelum menulis kode implementasi, pastikan lingkungan server dan workstation pengembangan Anda memenuhi spesifikasi teknis berikut:

  • Sistem Operasi: Ubuntu 22.04 LTS / Debian 12 / macOS Ventura+
  • Runtime Environment: Node.js (Versi LTS >= 18.x)
  • Package Manager: npm (>= 9.x) atau pnpm
  • Message Broker: Redis Server (Versi >= 7.0)
  • Web Server / Reverse Proxy: Nginx (untuk terminasi SSL dan WebSocket Proxying)

Verifikasi Instalasi Sistem

Jalankan perintah berikut pada terminal Linux/Unix Anda untuk memastikan semua dependensi siap:

node -v
npm -v
redis-cli --version
nginx -v

Pastikan Redis berjalan sebagai layanan sistem dan aktif saat booting:

sudo systemctl enable --now redis-server
sudo systemctl status redis-server

5. Panduan Implementasi Langkah demi Langkah

Pada bagian ini, kita akan membangun aplikasi Node.js lengkap menggunakan TypeScript, Apollo Server (v4), GraphQL-WS, dan Redis Pub/Sub. Struktur direktori proyek adalah sebagai berikut:

graphql-realtime-app/
├── package.json
├── tsconfig.json
├── .env
└── src/
    ├── index.ts
    ├── schema.ts
    ├── resolvers.ts
    └── redis.ts

Langkah 1: Inisialisasi Proyek dan Instalasi Dependensi

Buat direktori baru dan inisialisasi proyek Node.js:

mkdir graphql-realtime-app && cd graphql-realtime-app
npm init -y

Instal dependensi utama yang dibutuhkan untuk GraphQL, Apollo Server, WebSocket, dan Redis:

npm install @apollo/server graphql graphql-ws ws ioredis graphql-redis-subscriptions dotenv
npm install -D typescript @types/node @types/ws ts-node nodemon

Inisialisasi konfigurasi TypeScript:

npx tsc --init

Langkah 2: Konfigurasi TypeScript (tsconfig.json)

Sesuaikan file tsconfig.json untuk mendukung kompilasi modern:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"]
}

Langkah 3: Konfigurasi Variabel Lingkungan (.env)

Buat file .env di akar direktori:

PORT=4000
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

Langkah 4: Modul Koneksi Redis (src/redis.ts)

Buat klien Redis menggunakan ioredis dan inisialisasi RedisPubSub:

import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
import dotenv from 'dotenv';

dotenv.config();

const options = {
  host: process.env.REDIS_HOST || '127.0.0.1',
  port: Number(process.env.REDIS_PORT) || 6379,
  retryStrategy: (times: number) => {
    // Reconnect setelah jeda eksponensial
    const delay = Math.min(times * 50, 2000);
    return delay;
  },
};

// Klien Redis terpisah untuk Publisher dan Subscriber
const pubSubClient = new Redis(options);
const subClient = new Redis(options);

pubSubClient.on('error', (err) => console.error('Redis Pub Client Error:', err));
subClient.on('error', (err) => console.error('Redis Sub Client Error:', err));

export const pubsub = new RedisPubSub({
  publisher: pubSubClient,
  subscriber: subClient,
});

Langkah 5: Definisi Skema GraphQL (src/schema.ts)

Definisikan tipe data, query, mutation, dan subscription:

export const typeDefs = `#graphql
  type Message {
    id: ID!
    content: String!
    sender: String!
    createdAt: String!
  }

  type Query {
    messages: [Message!]!
  }

  input CreateMessageInput {
    content: String!
    sender: String!
  }

  type Mutation {
    postMessage(input: CreateMessageInput!): Message!
  }

  type Subscription {
    messageCreated: Message!
  }
`;

Langkah 6: Implementasi Resolvers (src/resolvers.ts)

Hubungkan mutasi dengan fungsi publikasi Redis dan tangkap event di bagian subscription:

import { pubsub } from './redis.js';

interface MessageInput {
  content: string;
  sender: string;
}

interface Message {
  id: string;
  content: string;
  sender: string;
  createdAt: string;
}

// Database in-memory sementara untuk menyimpan riwayat pesan
const messages: Message[] = [];

const MESSAGE_CREATED_CHANNEL = 'MESSAGE_CREATED';

export const resolvers = {
  Query: {
    messages: () => messages,
  },
  Mutation: {
    postMessage: async (_: unknown, { input }: { input: MessageInput }) => {
      const newMessage: Message = {
        id: String(Date.now()),
        content: input.content,
        sender: input.sender,
        createdAt: new Date().toISOString(),
      };

      messages.push(newMessage);

      // Publikasikan event ke Redis Pub/Sub
      await pubsub.publish(MESSAGE_CREATED_CHANNEL, { messageCreated: newMessage });

      return newMessage;
    },
  },
  Subscription: {
    messageCreated: {
      // Berlangganan ke channel Redis melalui AsyncIterator
      subscribe: () => pubsub.asyncIterator([MESSAGE_CREATED_CHANNEL]),
    },
  },
};

Langkah 7: Titik Masuk Server Apollo & WebSocket (src/index.ts)

Gabungkan Express, Apollo Server, dan transport WebSocket (graphql-ws):

import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { createServer } from 'http';
import express from 'express';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/useServer';
import { makeExecutableSchema } from '@graphql-tools/schema';
import cors from 'cors';
import dotenv from 'dotenv';
import { typeDefs } from './schema.js';
import { resolvers } from './resolvers.js';

dotenv.config();

async function startServer() {
  const app = express();
  const httpServer = createServer(app);

  // Buat skema executable GraphQL
  const schema = makeExecutableSchema({ typeDefs, resolvers });

  // Konfigurasi WebSocket Server untuk Subscriptions
  const wsServer = new WebSocketServer({
    server: httpServer,
    path: '/graphql',
  });

  const serverCleanup = useServer({ schema }, wsServer);

  // Konfigurasi Apollo Server
  const server = new ApolloServer({
    schema,
    plugins: [
      ApolloServerPluginDrainHttpServer({ httpServer }),
      {
        async serverWillStart() {
          return {
            async drainServer() {
              await serverCleanup.dispose();
            },
          };
        },
      },
    ],
  });

  await server.start();

  app.use(
    '/graphql',
    cors<cors.CorsRequest>(),
    express.json(),
    expressMiddleware(server),
  );

  const PORT = process.env.PORT || 4000;

  httpServer.listen(PORT, () => {
    console.log(`🚀 Server berjalan di http://localhost:${PORT}/graphql`);
    console.log(`🔌 WebSocket Subscription berjalan di ws://localhost:${PORT}/graphql`);
  });
}

startServer().catch((err) => {
  console.error('Gagal menjalankan server:', err);
});

6. Bedah Kode & Penjelasan Detail Baris Kunci

Mari kita analisis bagian-bagian paling krusial dari implementasi kode di atas:

  • Pemisahan Klien Publisher dan Subscriber (src/redis.ts):
    • Fungsi: Mengapa kita menginisialisasi dua koneksi Redis terpisah (pubSubClient dan subClient)? Berdasarkan dokumentasi dan spesifikasi pustaka klien Redis (ioredis), ketika koneksi Redis berada dalam mode Subscriber, koneksi tersebut tidak dapat menjalankan perintah lain selain mendengarkan pesan dari channel yang diikutinya. Memisahkan instance publisher dan subscriber adalah praktik wajib (mandatory best practice) untuk menghindari konflik status perintah di level soket TCP.
  • Penggunaan graphql-ws vs subscriptions-transport-ws:
    • Fungsi: Kami menggunakan pustaka graphql-ws karena subscriptions-transport-ws telah dinyatakan usang (deprecated) oleh komunitas pengembang sejak beberapa tahun lalu. graphql-ws mematuhi protokol WebSocket GraphQL standar yang lebih modern, aman, dan mendukung manajemen keep-alive yang handal untuk mencegah pemutusan koneksi sepihak oleh NAT/firewall jaringan.
  • ApolloServerPluginDrainHttpServer:
    • Fungsi: Plugin ini memastikan bahwa ketika server menerima sinyal terminasi (seperti SIGTERM dari proses Kubernetes atau systemd saat graceful shutdown), server akan menutup koneksi HTTP dan WebSocket secara bersih tanpa memutus proses yang sedang berjalan secara paksa (abrupt disconnection).

7. Skenario Produksi & Studi Kasus Dunia Nyata

Dalam lingkungan produksi nyata, arsitektur ini biasanya diletakkan di belakang web server Nginx sebagai Reverse Proxy dan Load Balancer. Agar Nginx dapat meneruskan (proxy) koneksi WebSocket dengan benar, konfigurasi blok server Nginx harus disetel secara khusus.

Konfigurasi Nginx untuk WebSocket & HTTP Proxy (/etc/nginx/sites-available/graphql-api)

upstream graphql_cluster {
    # Menggunakan algoritma least_conn untuk distribusi beban WebSocket yang merata
    least_conn;
    server 127.0.0.1:4000;
    server 127.0.0.1:4001;
}

server {
    listen 80;
    server_name api.enterprise.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.enterprise.com;

    ssl_certificate /etc/letsencrypt/live/api.enterprise.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.enterprise.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location /graphql {
        proxy_pass http://graphql_cluster;
        proxy_http_version 1.1;
        
        # Header krusial untuk upgrade koneksi ke WebSocket
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeout diset panjang agar koneksi WebSocket tidak terputus prematur
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

[!IMPORTANT] Pengaturan proxy_set_header Upgrade $http_upgrade; dan proxy_set_header Connection "upgrade"; sangat mutlak diperlukan. Tanpa header ini, Nginx akan memperlakukan permintaan WebSocket sebagai permintaan HTTP standar dan mengembalikan kode status kesalahan 400 Bad Request atau memutus koneksi secara sepihak.


8. Common Pitfalls, Anti-Patterns & Solusi

Dalam pengalaman mengelola infrastruktur sistem skala besar, berikut adalah kesalahan fatal yang sering dilakukan oleh pengembang saat mengimplementasikan GraphQL Subscriptions dengan Redis:

  1. Menggunakan Satu Klien Redis untuk Pub dan Sub:
    • Kesalahan: Menggunakan satu instance objek Redis untuk menjalankan pubsub.publish() dan asyncIterator().
    • Dampak: Menyebabkan error runtime ERR Can't execute command while subscribed ketika instance Redis sibuk mendengarkan channel.
    • Solusi: Selalu inisialisasi koneksi terpisah khusus untuk publisher dan subscriber seperti yang ditunjukkan pada modul src/redis.ts di atas.
  2. Tidak Mengimplementasikan Autentikasi pada WebSocket Handshake:
    • Kesalahan: Membiarkan endpoint WebSocket terbuka tanpa verifikasi token JWT pada tahap awal jabat tangan (connection_init).
    • Dampak: Memungkinkan klien anonim atau berbahaya melakukan denial of service (DoS) dengan membuka ribuan koneksi WebSocket palsu atau mendengarkan data sensitif melalui subscription.
    • Solusi: Validasi token autentikasi di dalam fungsi onConnect atau parameter context pada konfigurasi useServer di graphql-ws.
  3. Memory Leak akibat Ketidakpahaman Lifecycle Klien:
    • Kesalahan: Menyimpan referensi koneksi klien secara manual di dalam memori global tanpa mekanisme garbage collection saat klien terputus secara mendadak (abrupt disconnection).
    • Dampak: Penggunaan memori RAM server meningkat secara linier hingga server mengalami Out Of Memory (OOM) Crash.
    • Solusi: Andalkan pustaka standar seperti graphql-ws yang secara otomatis mengelola pembersihan memori (cleanup) saat koneksi terputus.

9. Benchmark, Metrik Kinerja & Tuning Performa

Untuk memastikan sistem siap menghadapi beban produksi berskala tinggi, pengujian beban (load testing) harus dilakukan. Karena alat pengujian HTTP tradisional (seperti Apache Bench) tidak dirancang untuk mempertahankan koneksi WebSocket persisten, kita dapat menggunakan alat modern seperti Artillery.io atau skrip kustom berbasis Node.js menggunakan modul ws.

Skenario Pengujian Beban Menggunakan Artillery

Buat file konfigurasi pengujian load-test.yml:

config:
  target: "ws://localhost:4000/graphql"
  phases:
    - duration: 60
      arrivalRate: 50
      rampTo: 200
  engines:
    ws: {}
scenarios:
  - name: "Connect and Subscribe to Messages"
    engine: ws
    flow:
      - send: "{\"type\":\"connection_init\\
🏷️ Tags: #Nginx #High Concurrency #Performance #Sysadmin #Web Server