Tutorial lengkap belajar GraphQL dengan Apollo Client — queries, mutations, cache management, real-time subscriptions, dan optimasi performa dengan contoh kode praktis
GraphQL adalah bahasa query untuk API yang dikembangkan oleh Facebook (Meta) pada tahun 2012 dan dirilis sebagai open-source pada tahun 2015. GraphQL memungkinkan client untuk meminta data yang tepat dibutuhkan — tidak lebih, tidak kurang — dalam satu permintaan tunggal.
Berbeda dengan REST API yang menggunakan banyak endpoints berbeda, GraphQL menggunakan satu endpoint tunggal dengan fleksibilitas query yang tinggi. Ini mengatasi masalah over-fetching (mengambil data berlebihan) dan under-fetching (data kurang) yang sering terjadi di REST.
Konsep Inti GraphQL
Konsep
Penjelasan
Schema
Definisi tipe data dan hubungan antar entitas — blueprint dari API Anda
Query
Operasi untuk mengambil data (read) — seperti GET di REST
Mutation
Operasi untuk mengubah data (create, update, delete) — seperti POST/PUT/DELETE
Subscription
Operasi untuk mendengarkan perubahan data secara real-time
Type System
Sistem tipe yang ketat untuk memvalidasi data yang diminta
Resolver
Fungsi yang bertugas mengambil data untuk setiap field dalam schema
Struktur Schema GraphQL
Diagram: CLIENT (React App)
2. GraphQL vs REST API
Sebelum melangkah lebih jauh, mari kita pahami perbedaan mendasar antara GraphQL dan REST API agar Anda tahu kapan sebaiknya menggunakan masing-masing.
Aspek
REST API
GraphQL
Endpoints
Banyak (GET /users, POST /posts, dll.)
Satu (POST /graphql)
Data Fetching
Server menentukan respons
Client menentukan data yang diambil
Over-fetching
Sering terjadi — data berlebih
Tidak — ambil hanya yang dibutuhkan
Under-fetching
Perlu banyak request
Satu request untuk semua data
Versioning
Perlu v1, v2, dll.
Tidak perlu — evolve schema
Type Safety
Tergantung dokumentasi
Built-in type system
Real-time
Perlu WebSocket manual
Subscription built-in
Caching
Mudah (HTTP caching)
Perlu tool tambahan (Apollo Cache)
Learning Curve
🟢 Mudah
🟡 Sedang
💡 Kapan Menggunakan GraphQL?
GraphQL sangat cocok untuk:
Aplikasi dengan banyak relasi data yang kompleks
Frontend yang membutuhkan fleksibilitas query tinggi
Proyek dengan beberapa client (web, mobile, IoT)
Aplikasi real-time (chat, notifikasi, dashboard)
REST masih lebih baik untuk API sederhana, file upload, dan caching HTTP yang agresif.
3. Setup Apollo Client
Apollo Client adalah library state management paling populer untuk GraphQL yang bekerja sangat baik dengan React. Apollo Client menyediakan caching otomatis, pagination, optimistic UI, dan banyak fitur lainnya.
Instalasi
Bash
# Instal Apollo Client dan dependensinya
npm install @apollo/client graphql
# Atau dengan yarn
yarn add @apollo/client graphql
# Atau dengan pnpm
pnpm add @apollo/client graphql
# Output:
# + @apollo/client@3.10.x
# + graphql@16.x.x
Konfigurasi ApolloProvider
JavaScript — src/apollo/client.js
import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';
// HTTP Link untuk queries dan mutations
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
credentials: 'include', // Kirim cookies
});
// WebSocket Link untuk subscriptions
const wsLink = new GraphQLWsLink(
createClient({
url: 'ws://localhost:4000/graphql',
connectionParams: () => {
const token = localStorage.getItem('auth-token');
return { authToken: token };
},
})
);
// Split link: kirim operasi ke link yang tepat
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink, // Jika subscription → WebSocket
httpLink // Jika query/mutation → HTTP
);
// Buat Apollo Client
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
semuaArtikel: {
// Merge untuk pagination
keyArgs: false,
merge(existing = [], incoming) {
return [...existing, ...incoming];
},
},
},
},
},
}),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});
export default client;
Wrap Aplikasi dengan ApolloProvider
JSX — src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import client from './apollo/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
);
// Sekarang semua komponen di dalam App bisa
// menggunakan hooks Apollo (useQuery, useMutation, dll.)
⚠️ Perhatian
Pastikan GraphQL server sudah berjalan sebelum menjalankan client. Untuk development, Anda bisa menggunakan Apollo Server atau GraphQL Yoga sebagai server lokal.
4. Queries: Mengambil Data
Query adalah operasi GraphQL untuk mengambil data. Dengan Apollo Client, Anda menggunakan useQuery hook untuk mengirim query dan mendapatkan hasilnya secara reaktif.
Definisi Query dengan gql
JavaScript — src/graphql/queries.js
import { gql } from '@apollo/client';
// Query untuk mengambil semua artikel
export const GET_ALL_ARTIKEL = gql`
query GetAllArtikel($limit: Int, $offset: Int) {
semuaArtikel(limit: $limit, offset: $offset) {
id
judul
konten
tag
dipublikasikan
views
penulis {
id
nama
email
}
}
}
`;
// Query untuk mengambil satu artikel berdasarkan ID
export const GET_ARTIKEL = gql`
query GetArtikel($id: ID!) {
artikel(id: $id) {
id
judul
konten
tag
dipublikasikan
views
createdAt
penulis {
id
nama
email
}
}
}
`;
// Query untuk mengambil semua user
export const GET_USERS = gql`
query GetUsers {
users {
id
nama
email
createdAt
artikel {
id
judul
}
}
}
`;
// Query untuk mengambil satu user
export const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
nama
email
artikel {
id
judul
views
}
}
}
`;
import { useLazyQuery } from '@apollo/client';
import { GET_ARTIKEL } from '../graphql/queries';
import { useState } from 'react';
function CariArtikel() {
const [idArtikel, setIdArtikel] = useState('');
// useLazyQuery: query tidak dieksekusi sampai fungsi dipanggil
const [getArtikel, { loading, error, data }] = useLazyQuery(
GET_ARTIKEL,
{
// Simpan hasil ke cache
fetchPolicy: 'cache-and-network',
}
);
const handleCari = () => {
if (idArtikel.trim()) {
getArtikel({ variables: { id: idArtikel.trim() } });
}
};
return (
<div className="cari-artikel">
<h2>Cari Artikel by ID</h2>
<div className="search-form">
<input
type="text"
value={idArtikel}
onChange={(e) => setIdArtikel(e.target.value)}
placeholder="Masukkan ID artikel..."
/>
<button onClick={handleCari} disabled={loading}>
{loading ? 'Mencari...' : '🔍 Cari'}
</button>
</div>
{error && <p className="error">{error.message}</p>}
{data?.artikel && (
<div className="artikel-detail">
<h3>{data.artikel.judul}</h3>
<p>{data.artikel.konten}</p>
<p>Penulis: {data.artikel.penulis.nama}</p>
<p>Views: {data.artikel.views}</p>
</div>
)}
</div>
);
}
export default CariArtikel;
5. Mutations: Mengubah Data
Mutation digunakan untuk membuat, memperbarui, atau menghapus data. Dengan Apollo Client, Anda menggunakan useMutation hook untuk melakukan operasi mutasi.
Definisi Mutations
JavaScript — src/graphql/mutations.js
import { gql } from '@apollo/client';
// Membuat user baru
export const CREATE_USER = gql`
mutation CreateUser($nama: String!, $email: String!) {
createUser(nama: $nama, email: $email) {
id
nama
email
createdAt
}
}
`;
// Memperbarui artikel
export const UPDATE_ARTIKEL = gql`
mutation UpdateArtikel(
$id: ID!
$judul: String
$konten: String
$tag: [String!]
$dipublikasikan: Boolean
) {
updateArtikel(
id: $id
judul: $judul
konten: $konten
tag: $tag
dipublikasikan: $dipublikasikan
) {
id
judul
konten
tag
dipublikasikan
}
}
`;
// Menghapus artikel
export const DELETE_ARTIKEL = gql`
mutation DeleteArtikel($id: ID!) {
deleteArtikel(id: $id)
}
`;
// Membuat artikel baru
export const CREATE_ARTIKEL = gql`
mutation CreateArtikel(
$judul: String!
$konten: String!
$tag: [String!]!
$dipublikasikan: Boolean
) {
createArtikel(
judul: $judul
konten: $konten
tag: $tag
dipublikasikan: $dipublikasikan
) {
id
judul
konten
penulis {
id
nama
}
}
}
`;
Optimistic UI memungkinkan UI diperbarui sebelum server merespons. Ini membuat aplikasi terasa lebih responsif. Jika server mengembalikan error, Apollo akan otomatis rollback perubahan ke state sebelumnya.
6. Cache Management
Salah satu fitur terbaik Apollo Client adalah caching otomatis. Setiap query yang dikirimkan, hasilnya otomatis disimpan di cache lokal. Ini mengurangi request ke server dan membuat UI sangat responsif.
Cara Kerja Cache
Diagram: Apollo Cache Flow
Query
sent
Cache
check
Apollo Cache (InMemoryCache)
Cache Normalized:
"Artikel:1" → { id:1, judul:"React", ... }
"Artikel:2" → { id:2, judul:"Node", ... }
"User:1" → { id:1, nama:"Budi", ... }
When Query is sent:
►
► Network
(jika
perlu)
Fetch Policies:
• cache-first: cek cache dulu, network jika miss
• cache-only: hanya dari cache
• network-only: hanya dari network
• no-cache: tanpa cache sama sekali
• cache-and-network: keduanya (update bg)
Cache Type Policies
JavaScript — Cache Configuration
import { InMemoryCache, makeVar } from '@apollo/client';
// Reactive variables — global state di luar cache
export const isLoggedInVar = makeVar(false);
export const currentUserVar = makeVar(null);
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
// Pagination: merge results
semuaArtikel: {
keyArgs: ['filter', 'tag'], // Cache terpisah per filter/tag
merge(existing = [], incoming, { args }) {
if (args?.offset === 0) return incoming; // Reset
return [...existing, ...incoming];
},
read(existing, { args }) {
if (args?.offset && !existing) return undefined;
return existing;
},
},
// Custom field: gabungkan data dari cache
artikelPopuler: {
read(existing) {
return existing || [];
},
},
// Reactive variable field
isLoggedIn: {
read() {
return isLoggedInVar();
},
},
currentUser: {
read() {
return currentUserVar();
},
},
},
},
// Merge strategy untuk Artikel type
Artikel: {
keyFields: ['id'], // Default, bisa custom: ['id', 'versi']
fields: {
views: {
// Optimistic update untuk views count
merge(existing = 0, incoming) {
return Math.max(existing, incoming);
},
},
},
},
// Merge strategy untuk User type
User: {
keyFields: ['id'],
fields: {
artikel: {
merge(existing = [], incoming) {
return incoming; // Replace, bukan merge
},
},
},
},
},
});
export default cache;
Subscription memungkinkan Anda mendengarkan perubahan data secara real-time menggunakan WebSocket. Ini sangat berguna untuk fitur seperti chat, notifikasi live, dan dashboard monitoring.
Definisi Subscriptions
JavaScript — src/graphql/subscriptions.js
import { gql } from '@apollo/client';
// Subscription: artikel baru
export const ON_ARTIKEL_BARU = gql`
subscription OnArtikelBaru {
artikelBaru {
id
judul
konten
tag
dipublikasikan
penulis {
id
nama
}
}
}
`;
// Subscription: user online
export const ON_USER_ONLINE = gql`
subscription OnUserOnline {
userOnline {
id
nama
email
}
}
`;
// Subscription: perubahan pada artikel tertentu
export const ON_ARTIKEL_UPDATED = gql`
subscription OnArtikelUpdated($artikelId: ID!) {
artikelUpdated(id: $artikelId) {
id
judul
konten
views
dipublikasikan
}
}
`;
Untuk subscriptions berfungsi, server GraphQL perlu mendukung WebSocket. Apollo Server menggunakan graphql-ws package. Pastikan server mengimplementasikan PubSub untuk publish data real-time ke subscriber.
8. Error Handling
Menangani error dengan baik sangat penting dalam aplikasi GraphQL. Apollo Client menyediakan beberapa cara untuk menangani error — dari tingkat komponen hingga global.
Error Handling per Komponen
JSX — Error Handling
import { useQuery, useMutation, ApolloError } from '@apollo/client';
import { GET_ALL_ARTIKEL } from '../graphql/queries';
function KomponenDenganErrorHandling() {
const { loading, error, data } = useQuery(GET_ALL_ARTIKEL, {
// Error policy: 'none' (default), 'all' (termasuk partial data)
errorPolicy: 'all',
// Retry on network error
onError: (error) => {
console.error('Query error:', error);
if (error.networkError) {
// Tampilkan toast: "Koneksi bermasalah"
showToast('Koneksi bermasalah, coba lagi nanti');
}
if (error.graphQLErrors) {
error.graphQLErrors.forEach((err) => {
console.log('GraphQL Error:', err.message, err.extensions);
if (err.extensions?.code === 'UNAUTHENTICATED') {
// Redirect ke login
window.location.href = '/login';
}
});
}
},
});
if (loading) return <LoadingSpinner />;
// error.networkError: masalah jaringan
// error.graphQLErrors: error dari GraphQL server
// error.message: pesan error gabungan
if (error) {
return (
<div className="error-container">
<h3>❌ Terjadi Kesalahan</h3>
{error.networkError && (
<p className="network-error">
Koneksi ke server gagal. Periksa koneksi internet Anda.
</p>
)}
{error.graphQLErrors.map(({ message, extensions }, idx) => (
<div key={idx} className="graphql-error">
<p>{message}</p>
{extensions?.code && (
<span className="error-code">Code: {extensions.code}</span>
)}
</div>
))}
<button onClick={() => window.location.reload()}>
🔄 Coba Lagi
</button>
</div>
);
}
return <DaftarArtikel data={data} />;
}
Global Error Link
JavaScript — src/apollo/errorLink.js
import { onError } from '@apollo/client/link/error';
import { fromPromise } from '@apollo/client';
import { refreshToken } from './auth';
const errorLink = onError(
({ graphQLErrors, networkError, operation, forward }) => {
// Handle GraphQL errors
if (graphQLErrors) {
for (const err of graphQLErrors) {
switch (err.extensions?.code) {
case 'UNAUTHENTICATED':
// Token expired, coba refresh
return fromPromise(
refreshToken().catch(() => {
// Refresh gagal → logout
localStorage.removeItem('auth-token');
window.location.href = '/login';
return null;
})
).flatMap(() => forward(operation));
case 'FORBIDDEN':
console.error('Akses ditolak:', err.message);
break;
case 'BAD_USER_INPUT':
console.warn('Input tidak valid:', err.message);
break;
default:
console.error('GraphQL Error:', err.message);
}
}
}
// Handle network errors
if (networkError) {
console.error('Network Error:', networkError);
// Retry logic untuk 5xx errors
if (networkError.statusCode >= 500) {
console.log('Server error, akan dicoba lagi...');
}
}
}
);
export default errorLink;
9. Optimasi & Best Practices
Untuk membangun aplikasi GraphQL yang performan dan mudah di-maintain, berikut adalah best practices yang harus diterapkan:
Fragment Reuse
JavaScript — Fragments
import { gql } from '@apollo/client';
// Definisikan fragment untuk reuse
export const ARTIKEL_CORE_FIELDS = gql`
fragment ArtikelCoreFields on Artikel {
id
judul
konten
tag
dipublikasikan
views
}
`;
export const PENULIS_FIELDS = gql`
fragment PenulisFields on User {
id
nama
email
}
`;
// Gunakan fragment di queries
export const GET_ALL_ARTIKEL = gql`
${ARTIKEL_CORE_FIELDS}
${PENULIS_FIELDS}
query GetAllArtikel($limit: Int, $offset: Int) {
semuaArtikel(limit: $limit, offset: $offset) {
...ArtikelCoreFields
penulis {
...PenulisFields
}
}
}
`;
// Fragment juga bisa nested
export const ARTIKEL_DETAIL = gql`
${ARTIKEL_CORE_FIELDS}
${PENULIS_FIELDS}
query GetArtikelDetail($id: ID!) {
artikel(id: $id) {
...ArtikelCoreFields
createdAt
penulis {
...PenulisFields
artikel {
id
judul
views
}
}
}
}
`;