GraphQL adalah query language untuk API yang dikembangkan oleh Facebook (2015).
Perbedaan utama dengan REST:
| Aspek | REST | GraphQL |
|---|---|---|
| Endpoint | Banyak (/users, /posts) |
Satu (/graphql) |
| Data | Server tentukan | Client tentukan |
| Over-fetching | Sering terjadi | Tidak ada |
| Under-fetching | Perlu multiple request | Satu request cukup |
Schema & Types:
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
author: User!
}
Query (mengambil data):
query {
user(id: 1) {
name
email
posts {
title
}
}
}
Client memilih field yang dibutuhkan — tidak ada data berlebihan.
Mutation (mengubah data):
mutation {
createUser(name: "Budi", email: "[email protected]") {
id
name
}
}
Resolver: Fungsi di server yang mengambil data untuk setiap field di schema.
Kapan pakai GraphQL vs REST:
- GraphQL: frontend butuh fleksibilitas tinggi, banyak relasi, mobile app (hemat bandwidth)
- REST: API sederhana, caching mudah, tim kecil, public API
N+1 Problem di GraphQL — Tantangan Utama
GraphQL mudah menyebabkan N+1 query di database — lebih sering daripada REST! Karena client bisa request nested data apapun.
Contoh masalah:
query {
users { # 1 query: SELECT * FROM users
name
posts { # N query: untuk TIAP user, SELECT * FROM posts WHERE user_id = ?
title
}
}
}
Kalau ada 100 user, server eksekusi 1 + 100 = 101 query. Satu GraphQL request yang terlihat simple bisa menumbangkan database.
Solusi: DataLoader
DataLoader adalah library yang batch + cache query di dalam 1 request tick.
import DataLoader from "dataloader";
// Inisialisasi per-request (penting: jangan share antar request!)
function createPostsByUserLoader() {
return new DataLoader(async (userIds) => {
// 1 query saja untuk semua user IDs
const posts = await db.query(
"SELECT * FROM posts WHERE user_id = ANY($1)",
[userIds]
);
// Group hasil sesuai urutan userIds
return userIds.map(id => posts.filter(p => p.user_id === id));
});
}
// Di resolver
const resolvers = {
User: {
posts: (user, args, context) => {
return context.postsByUserLoader.load(user.id);
// DataLoader batch semua .load() call dalam 1 tick → 1 SQL query
},
},
};
// Apollo Server — per-request context
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
postsByUserLoader: createPostsByUserLoader(),
}),
});
Hasilnya: 101 query → 2 query (users + batched posts). DataLoader adalah must-have untuk production GraphQL.
Query Depth & Complexity Limits (Security)
GraphQL flexibility = attack surface. Malicious client bisa kirim query yang nested sangat dalam:
query EvilQuery {
users {
posts {
author {
posts {
author {
posts {
# ...100 level lebih
}
}
}
}
}
}
}
Eksponensial — server crash dalam detik. Solusi:
import depthLimit from "graphql-depth-limit";
import { createComplexityLimitRule } from "graphql-validation-complexity";
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(7), // max 7 level nesting
createComplexityLimitRule(1000, { // compute cost total
onCost: (cost) => console.log(`Query cost: ${cost}`),
}),
],
});
Setiap field punya cost. Query "mahal" (list, nested) ditolak sebelum eksekusi.
Subscriptions — Real-Time
GraphQL bukan cuma query/mutation. Subscription = push data dari server via WebSocket saat event terjadi.
type Subscription {
newMessage(chatId: ID!): Message!
}
import { PubSub } from "graphql-subscriptions";
const pubsub = new PubSub();
const resolvers = {
Subscription: {
newMessage: {
subscribe: (_, { chatId }) => pubsub.asyncIterator(`NEW_MSG_${chatId}`),
},
},
Mutation: {
sendMessage: async (_, { chatId, text }) => {
const msg = await db.messages.create({ chatId, text });
pubsub.publish(`NEW_MSG_${chatId}`, { newMessage: msg });
return msg;
},
},
};
Client (apollo-client):
useSubscription(NEW_MESSAGE_SUBSCRIPTION, { variables: { chatId } });
// React component auto-update saat message baru masuk
Cocok untuk: chat, notifications, live dashboards, multiplayer.
Pagination — Cursor-Based (Relay Spec)
REST boleh pakai ?page=5. GraphQL best practice: cursor-based (Relay connection spec).
type Query {
posts(first: Int, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}
Keuntungan: stable saat data ada insert (vs offset yang bisa duplicate/skip).
Error Handling
GraphQL tidak pakai HTTP status untuk error bisnis (selalu 200). Error di dalam response body:
{
"data": { "user": null },
"errors": [
{ "message": "User not found", "path": ["user"], "extensions": { "code": "NOT_FOUND" } }
]
}
Client cek errors array + extensions.code untuk branching. Untuk error yang perlu stop request sepenuhnya (auth), pakai HTTP 401 di level transport.