Menjelajahi paradigma API modern: GraphQL dengan schema dan resolver yang memberi client kendali penuh atas data, perbandingan jujur dengan REST, keamanan GraphQL, dan kapan memilih yang mana

Selama series ini kita membangun REST API (episode 4). Episode 24 memperkenalkan paradigma lain yang menantangnya: GraphQL — dan juga melihat sekilas protokol modern lain seperti tRPC. Bukan untuk menggantikan REST, melainkan untuk memahami trade-off-nya: GraphQL bukan "REST yang lebih baik", ia memecahkan masalah yang berbeda.
Mengapa penting? Karena masalah paling nyata REST adalah over-fetching dan under-fetching: mobile app butuh 3 field tapi API mengembalikan 20, atau sebuah layar butuh 4 endpoint padahal satu cukup. Di aplikasi mobile dengan bandwidth terbatas, ini berdampak besar. GraphQL menjawab dengan satu endpoint yang mengembalikan persis apa yang diminta.
GraphQL adalah query language + runtime. Tiga konsep:
type Product {
id: ID!
name: String!
price: Float!
stock: Int!
reviews: [Review!]
}
type Review {
id: ID!
rating: Int!
comment: String
author: String!
}
type Query {
product(id: ID!): Product
products(category: String, limit: Int = 20): [Product!]!
}
type Mutation {
createReview(productId: ID!, rating: Int!, comment: String): Review!
}! menandakan field non-null — kontrak yang ter-enforce. Schema ini adalah dokumentasi hidup sekaligus validasi otomatis (mirip protobuf gRPC di episode 13).
Resolver menentukan dari mana data tiap field diambil:
export const resolvers = {
Query: {
product: async (_parent, { id }: { id: string }) => {
const { rows } = await db.query(
"SELECT * FROM products WHERE id = $1", [id])
return rows[0]
},
products: async (_parent, { category, limit }: any) => {
const { rows } = await db.query(
`SELECT * FROM products
WHERE ($1::text IS NULL OR category = $1)
ORDER BY created_at DESC LIMIT $2`,
[category, limit],
)
return rows
},
},
Mutation: {
createReview: async (_parent, args: any, ctx) => {
requireAuth(ctx) // episode 7
const { rows } = await db.query(
`INSERT INTO reviews (product_id, rating, comment, author)
VALUES ($1, $2, $3, $4) RETURNING *`,
[args.productId, args.rating, args.comment, ctx.user.name],
)
return rows[0]
},
},
Product: {
reviews: async (parent) => {
const { rows } = await db.query(
"SELECT * FROM reviews WHERE product_id = $1", [parent.id])
return rows
},
},
}Perhatikan: resolver Product.reviews dipanggil hanya jika client memintanya. Inilah kekuatan GraphQL — biaya query ditentukan permintaan client, bukan fixed endpoint.
| Aspek | Penjelasan |
|---|---|
| Anti over/under-fetching | Client meminta persis yang dibutuhkan |
| Satu endpoint | /graphql untuk semua; tidak ada versioning URL |
| Schema ter-enforce | Type-safe; perubahan breaking terdeteksi |
| Tooling | Introspection, autocomplete, type generation |
| Aspek | Penjelasan |
|---|---|
| Kompleksitas | Schema + resolvers + cache (data-loader) |
| Cache HTTP sulit | GET tunggal, POST semuanya — CDN HTTP cache (episode 9) tidak langsung jalan |
| N+1 resolver | Setiap field = query potensial; butuh DataLoader |
| Abuse query | Query dalam bisa meledakkan server; butuh batas depth/complexity |
Note
N+1 di GraphQL: products lalu reviews per produk = 1 + N query, sama seperti anti-pattern di episode 9. Solusi standar: DataLoader — mengelompokkan dan mem-batch query per request. GraphQL yang tidak memakai DataLoader akan jauh lebih lambat dari REST untuk kasus yang sama.
GraphQL memperkenalkan vektor serangan baru:
Query bersarang dalam bisa membebani server. Batasi kedalaman:
import depthLimit from "graphql-depth-limit"
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(5)],
})Field mahal (yang trigger agregasi besar) diberi "bobot"; request yang melampaui total bobot ditolak.
Introspection memberi peta seluruh schema — matikan di produksi untuk mengurangi informasi bagi penyerang (episode 17-18).
Setiap resolver wajib requireAuth (episode 7), dan endpoint /graphql tetap di-rate limit (episode 20). Otorisasi per-field: user jangan bisa membaca field email milik orang lain.
| Kriteria | REST | GraphQL |
|---|---|---|
| Client beragam & mobile | Over-fetching jadi masalah | Kontrol penuh client |
| Cache HTTP/CDN | Alami | Sulit |
| Kesederhanaan | Tinggi | Perlu disiplin |
| Type contract | OpenAPI opsional | Wajib schema |
| Biaya backend | Rendah | DataLoader + complexity |
| Query kompleks lintas resource | Banyak round-trip | Satu query |
Aturan praktis:
curl -s -X POST http://localhost:4000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { products(limit: 2) { id name price reviews { rating } } }"
}'{
"data": {
"products": [
{ "id": "1", "name": "Keyboard", "price": 750000, "reviews": [] },
{ "id": "2", "name": "Mouse", "price": 150000, "reviews": [] }
]
}
}Satu request, satu response, tanpa field yang tidak diminta — perbandingan langsung dengan GET /v1/products REST yang selalu mengembalikan semua field.
Tip
Jika kalian menyukai type-safety GraphQL tapi tidak butuh query kompleks, lihat tRPC — RPC type-safe yang populer di ekosistem TypeScript end-to-end. Ia bahkan lebih ringan dari GraphQL untuk komunikasi internal. Pilihan urutannya: REST untuk publik, tRPC/gRPC untuk internal (episode 13), GraphQL untuk client yang kompleks.
N+1 membuat GraphQL lebih lambat dari REST. DataLoader wajib.
Query dalam mematikan server — sama seriusnya dengan DDoS (episode 20).
Satu resolver yang lupa requireAuth = kebocoran. Auth per-resolver, bukan sekali di endpoint.
CRUD sederhana lebih baik di REST. GraphQL menambah kompleksitas yang tidak terbayar.
Episode 24 membuka paradigma API modern: schema dan resolver GraphQL, keuntungan anti over-fetching, tantangan N+1 dan caching, keamanan query depth/complexity, serta aturan kapan REST vs GraphQL.
Inti yang harus dibawa pulang:
Di episode 25 selanjutnya kita akan mengoptimalkan performa sampai ke level produksi: performance tuning lanjutan — profiling, connection pooling, dan analisis bottleneck. Sampai jumpa di episode 25!