1. Pengenalan GraphQL
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
# Definisi tipe untuk User
type User {
id: ID!
nama: String!
email: String!
artikel: [Artikel!]!
createdAt: String
}
# Definisi tipe untuk Artikel
type Artikel {
id: ID!
judul: String!
konten: String!
penulis: User!
tag: [String!]!
dipublikasikan: Boolean!
views: Int
}
# Query root type
type Query {
users: [User!]!
user(id: ID!): User
artikel(id: ID!): Artikel
semuaArtikel(limit: Int, offset: Int): [Artikel!]!
}
# Mutation root type
type Mutation {
createUser(nama: String!, email: String!): User!
updateArtikel(id: ID!, judul: String, konten: String): Artikel!
deleteArtikel(id: ID!): Boolean!
}
# Subscription root type
type Subscription {
artikelBaru: Artikel!
userOnline: User!
}
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β CLIENT (React App) β
β β
β ββββββββββββββ ββββββββββββββ ββββββββββββββββββββββ β
β β Query β β Mutation β β Subscription β β
β β GET data β β MODIFY dataβ β REALTIME data β β
β βββββββ¬βββββββ βββββββ¬βββββββ ββββββββββ¬ββββββββββββ β
β β β β β
β βββββββββββββββββΌββββββββββββββββββββ β
β β β
β βββββββΌβββββββ β
β β Apollo β β
β β Client β β
β β (Cache) β β
β βββββββ¬βββββββ β
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ
β Single Endpoint
β POST /graphql
ββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββ
β βββββββΌβββββββ β
β β GraphQL β β
β β Server β β
β βββββββ¬βββββββ β
β β β
β ββββββββββββ ββββββββΌββββββ βββββββββββββββββββββ β
β β Database βββββ Resolvers ββββΊβ External APIs β β
β β (Postgresβ β β β (REST, gRPC, dll) β β
β ββββββββββββ ββββββββββββββ βββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
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 |
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
# 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
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
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.)
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
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
}
}
}
`;
Menggunakan useQuery Hook
import { useQuery } from '@apollo/client';
import { GET_ALL_ARTIKEL } from '../graphql/queries';
function DaftarArtikel() {
const { loading, error, data, refetch, fetchMore } = useQuery(
GET_ALL_ARTIKEL,
{
variables: { limit: 10, offset: 0 },
// fetchPolicy options:
// 'cache-first' β cek cache dulu (default)
// 'network-only' β selalu dari network
// 'cache-only' β hanya dari cache
// 'no-cache' β tidak menyimpan ke cache
// 'cache-and-network' β keduanya
fetchPolicy: 'cache-and-network',
// Polling untuk auto-refresh setiap 30 detik
// pollInterval: 30000,
}
);
if (loading) return <div className="skeleton">Memuat artikel...</div>;
if (error) return <p className="error">Error: {error.message}</p>;
const { semuaArtikel } = data;
const handleLoadMore = () => {
fetchMore({
variables: { offset: semuaArtikel.length },
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult.semuArtikel.length) return prev;
return {
semuaArtikel: [
...prev.semuArtikel,
...fetchMoreResult.semuArtikel,
],
};
},
});
};
return (
<div className="daftar-artikel">
<h2>Semua Artikel ({semuaArtikel.length})</h2>
<button onClick={() => refetch()} className="btn-refresh">
π Refresh Data
</button>
<div className="artikel-grid">
{semuaArtikel.map((artikel) => (
<div key={artikel.id} className="artikel-card">
<h3>{artikel.judul}</h3>
<p>Penulis: {artikel.penulis.nama}</p>
<p>Views: {artikel.views}</p>
<div className="tags">
{artikel.tag.map((t) => (
<span key={t} className="tag">{t}</span>
))}
</div>
</div>
))}
</div>
<button onClick={handleLoadMore} className="btn-loadmore">
Muat Lebih Banyak...
</button>
</div>
);
}
export default DaftarArtikel;
Lazy Query: Query yang Dipicu Manual
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
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
}
}
}
`;
Menggunakan useMutation Hook
import { useState } from 'react';
import { useMutation } from '@apollo/client';
import { CREATE_ARTIKEL } from '../graphql/mutations';
import { GET_ALL_ARTIKEL } from '../graphql/queries';
function BuatArtikel() {
const [formData, setFormData] = useState({
judul: '',
konten: '',
tag: '',
dipublikasikan: false,
});
// useMutation mengembalikan [mutateFunction, { data, loading, error }]
const [createArtikel, { loading, error }] = useMutation(
CREATE_ARTIKEL,
{
// Refetch queries setelah mutation berhasil
refetchQueries: [
{ query: GET_ALL_ARTIKEL, variables: { limit: 10, offset: 0 } },
],
// Atau gunakan update untuk manipulasi cache manual
// update(cache, { data: { createArtikel } }) {
// const existing = cache.readQuery({
// query: GET_ALL_ARTIKEL,
// variables: { limit: 10, offset: 0 },
// });
// cache.writeQuery({
// query: GET_ALL_ARTIKEL,
// variables: { limit: 10, offset: 0 },
// data: {
// semuaArtikel: [createArtikel, ...existing.semuArtikel],
// },
// });
// },
// Optimistic UI: tampilkan hasil sebelum server merespons
optimisticResponse: {
createArtikel: {
__typename: 'Artikel',
id: 'temp-' + Date.now(),
judul: formData.judul,
konten: formData.konten,
penulis: { __typename: 'User', id: 'current', nama: 'Anda' },
},
},
onCompleted: (data) => {
console.log('Artikel dibuat:', data.createArtikel);
setFormData({ judul: '', konten: '', tag: '', dipublikasikan: false });
},
onError: (err) => {
console.error('Gagal membuat artikel:', err.message);
},
}
);
const handleSubmit = (e) => {
e.preventDefault();
createArtikel({
variables: {
judul: formData.judul,
konten: formData.konten,
tag: formData.tag.split(',').map((t) => t.trim()),
dipublikasikan: formData.dipublikasikan,
},
});
};
return (
<form onSubmit={handleSubmit} className="form-artikel">
<h2>Buat Artikel Baru</h2>
<div className="form-group">
<label>Judul</label>
<input
type="text"
value={formData.judul}
onChange={(e) => setFormData({ ...formData, judul: e.target.value })}
required
/>
</div>
<div className="form-group">
<label>Konten</label>
<textarea
value={formData.konten}
onChange={(e) => setFormData({ ...formData, konten: e.target.value })}
rows={6}
required
/>
</div>
<div className="form-group">
<label>Tag (pisahkan koma)</label>
<input
type="text"
value={formData.tag}
onChange={(e) => setFormData({ ...formData, tag: e.target.value })}
placeholder="react, graphql, tutorial"
/>
</div>
<div className="form-group checkbox">
<label>
<input
type="checkbox"
checked={formData.dipublikasikan}
onChange={(e) =>
setFormData({ ...formData, dipublikasikan: e.target.checked })
}
/>
Langsung publikasikan
</label>
</div>
{error && <p className="error">Error: {error.message}</p>}
<button type="submit" disabled={loading}>
{loading ? 'Menyimpan...' : 'π Buat Artikel'}
</button>
</form>
);
}
export default BuatArtikel;
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
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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: β
β ββββββββββββ βββββββββββ βββββββββββββ β
β β Query βββββΊβ Cache βββββΊβ Network β β
β β sent β β check β β (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
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;
Manual Cache Manipulation
import { useApolloClient } from '@apollo/client';
function CacheController() {
const client = useApolloClient();
// Baca data dari cache
const bacaCache = () => {
const cached = client.readQuery({
query: GET_ALL_ARTIKEL,
variables: { limit: 10, offset: 0 },
});
console.log('Cache contents:', cached);
};
// Tulis data ke cache
const tulisCache = () => {
client.writeQuery({
query: GET_ALL_ARTIKEL,
variables: { limit: 10, offset: 0 },
data: {
semuaArtikel: [
{
__typename: 'Artikel',
id: '99',
judul: 'Ditulis langsung ke cache',
konten: 'Ini data manipulasi cache',
tag: ['cache'],
dipublikasikan: true,
views: 0,
penulis: { __typename: 'User', id: '1', nama: 'Admin' },
},
],
},
});
};
// Baca fragment dari cache
const bacaFragment = () => {
const fragment = client.readFragment({
id: 'Artikel:1', // Cache ID: TypeName:KeyValue
fragment: gql`
fragment DetailArtikel on Artikel {
id
judul
views
}
`,
});
console.log('Fragment data:', fragment);
};
// Reset seluruh cache
const resetCache = async () => {
await client.resetStore();
console.log('Cache di-reset!');
};
// Hapus cache tertentu
const hapusCacheItem = (artikelId) => {
client.cache.evict({ id: `Artikel:${artikelId}` });
client.cache.gc(); // Garbage collection
};
return (
<div className="cache-controls">
<button onClick={bacaCache}>π Baca Cache</button>
<button onClick={tulisCache}>βοΈ Tulis Cache</button>
<button onClick={bacaFragment}>π§© Baca Fragment</button>
<button onClick={resetCache}>π Reset Cache</button>
</div>
);
}
7. Subscriptions: Real-time Data
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
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
}
}
`;
Menggunakan useSubscription Hook
import { useEffect, useState } from 'react';
import { useSubscription, useApolloClient } from '@apollo/client';
import { ON_ARTIKEL_BARU, ON_USER_ONLINE } from '../graphql/subscriptions';
import { GET_ALL_ARTIKEL } from '../graphql/queries';
function NotifikasiRealtime() {
const [notifikasi, setNotifikasi] = useState([]);
const client = useApolloClient();
// Subscription untuk artikel baru
const { data: artikelData, loading: artikelLoading } = useSubscription(
ON_ARTIKEL_BARU,
{
onSubscriptionData: ({ subscriptionData }) => {
const artikel = subscriptionData.data.artikelBaru;
console.log('Artikel baru diterima:', artikel);
// Tambahkan notifikasi
setNotifikasi((prev) => [
{ id: Date.now(), type: 'artikel', message: `Artikel baru: "${artikel.judul}" oleh ${artikel.penulis.nama}` },
...prev,
]);
// Update cache: tambahkan artikel baru ke daftar
try {
const cached = client.readQuery({
query: GET_ALL_ARTIKEL,
variables: { limit: 10, offset: 0 },
});
if (cached) {
client.writeQuery({
query: GET_ALL_ARTIKEL,
variables: { limit: 10, offset: 0 },
data: {
semuaArtikel: [artikel, ...cached.semuArtikel],
},
});
}
} catch (e) {
// Cache belum diinisialisasi
console.log('Cache belum siap, skip update');
}
},
}
);
// Subscription untuk user online
const { data: userData } = useSubscription(ON_USER_ONLINE, {
onSubscriptionData: ({ subscriptionData }) => {
const user = subscriptionData.data.userOnline;
setNotifikasi((prev) => [
{ id: Date.now(), type: 'user', message: `${user.nama} sedang online` },
...prev.slice(0, 19), // Simpan max 20 notifikasi
]);
},
});
const hapusNotifikasi = (id) => {
setNotifikasi((prev) => prev.filter((n) => n.id !== id));
};
return (
<div className="notifikasi-panel">
<h3>π Notifikasi Real-time</h3>
{artikelLoading && <p>Menunggu subscription...</p>}
{notifikasi.length === 0 ? (
<p className="no-data">Belum ada notifikasi</p>
) : (
<ul className="notifikasi-list">
{notifikasi.map((n) => (
<li key={n.id} className={`notif-item notif-${n.type}`}>
<span>{n.type === 'artikel' ? 'π' : 'π€'}</span>
<span>{n.message}</span>
<button onClick={() => hapusNotifikasi(n.id)}>β</button>
</li>
))}
</ul>
)}
</div>
);
}
export default NotifikasiRealtime;
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
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
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
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
}
}
}
}
`;
Code Generation dengan GraphQL Codegen
# Instal GraphQL Codegen
npm install -D @graphql-codegen/cli @graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo
# Buat konfigurasi
cat > codegen.yml << 'EOF'
schema: "http://localhost:4000/graphql"
documents: "src/**/*.{ts,tsx,graphql}"
generates:
src/generated/graphql.tsx:
plugins:
- typescript
- typescript-operations
- typescript-react-apollo
config:
withHooks: true
withComponent: false
withHOC: false
EOF
# Jalankan codegen
npx graphql-codegen
# Output: src/generated/graphql.tsx
# β Types dan hooks auto-generated dari schema!
# β useGetAllArtikelQuery(), useCreateArtikelMutation(), dll.
- Gunakan fragments untuk menghindari duplikasi field definitions
- Pilih fetch policy yang tepat β cache-first untuk data yang jarang berubah, network-only untuk data yang sering update
- Paginate dengan benar β gunakan cursor-based pagination untuk dataset besar
- Optimistic UI untuk operasi create/update agar UI terasa instan
- Codegen untuk type safety dan auto-generated hooks
- Persist queries di production untuk keamanan dan performa
- Monitor cache size β gunakan
client.cache.gc()secara berkala
Tips Performa
| Teknik | Penjelasan | Dampak |
|---|---|---|
| Persisted Queries | Kirim hash query, bukan query string | π’ Kurangi bandwidth 70%+ |
| Query Batching | Gabungkan beberapa query dalam satu request | π’ Kurangi HTTP requests |
| Fragment Colocation | Letakkan fragment dekat komponen yang menggunakannya | π‘ Mudah di-maintain |
| Lazy Queries | Hanya fetch saat dibutuhkan (on-demand) | π’ Hemat bandwidth |
| Cache Normalization | Data dinormalisasi otomatis oleh InMemoryCache | π’ Konsistensi data |
| Field Policies | Kustomisasi merge/read per field | π‘ Fine-grained control |
10. Quiz Pemahaman
Uji pemahaman Anda tentang GraphQL dan Apollo Client dengan quiz berikut:
1. Apa keunggulan utama GraphQL dibanding REST API?
2. Hook apa yang digunakan untuk mengambil data di Apollo Client?
3. Apa fungsi dari InMemoryCache di Apollo Client?
4. Operasi GraphQL apa yang digunakan untuk data real-time?
5. Apa yang dimaksud dengan "Optimistic UI" di Apollo Client?