GraphQL API dengan Apollo Server dan Prisma: Query dan Mutation untuk Aplikasi Modern
The REST API Limitations That Drive Us Toward GraphQL
Dalam pengembangan API modern, arsitektur REST telah menjadi standar selama bertahun-tahun. Namun seiring kompleksitas aplikasi bertambah, beberapa kelemahan mendasar mulai terasa. Masalah paling umum adalah over-fetching — ketika sebuah endpoint REST mengembalikan lebih banyak data daripada yang dibutuhkan client. Contohnya, endpoint /users mungkin mengembalikan seluruh profil pengguna dengan alamat, riwayat transaksi, dan preferensi, padahal client hanya perlu menampilkan nama dan email. Data yang tidak terpakai ini membuang bandwidth dan memperlambat waktu muat halaman.
Di sisi lain, under-fetching terjadi ketika satu tampilan membutuhkan data dari beberapa endpoint. Sebuah halaman profil mungkin perlu memanggil /users, /users/posts, dan /users/followers secara berurutan. Setiap request membutuhkan round trip terpisah, dan total waktu respons menjadi penjumlahan dari seluruh latensi tersebut. Masalah ini semakin parah pada aplikasi mobile dengan koneksi jaringan yang tidak stabil.
GraphQL hadir sebagai solusi untuk masalah-masalah ini. GraphQL bukanlah database query language, melainkan declarative query language untuk API yang memungkinkan client menentukan secara eksplisit data apa yang mereka butuhkan. Cukup satu endpoint yang menangani operasi baca (query) dan tulis (mutation). Client mengirimkan query yang mendeskripsikan struktur data yang diinginkan, dan server mengembalikan response yang persis sesuai dengan struktur tersebut. Ini menghilangkan over-fetching dan under-fetching sekaligus mengurangi jumlah round trip yang diperlukan.

Gambar: Perbandingan arsitektur REST yang membutuhkan banyak request round-trip dengan GraphQL yang cukup satu request — Sumber: [ByteByteGo](https://github.com/ByteByteGoHq/system-design-101)
Dalam artikel ini, kita akan membangun GraphQL API menggunakan Apollo Server sebagai runtime GraphQL dan Prisma sebagai ORM untuk mengakses database. Kombinasi ini memberikan developer experience yang sangat baik — Apollo Server menangani eksekusi query GraphQL dengan baik, sementara Prisma menyediakan type-safe database client dengan auto-completion penuh.
Bootstrapping Apollo Server dengan Prisma Integration
Langkah pertama adalah menyiapkan proyek Node.js dengan TypeScript. Kita mulai dengan inisialisasi proyek dan instalasi dependensi utama.
mkdir graphql-api-demo && cd graphql-api-demo
npm init -y
npm install apollo-server prisma @prisma/client typescript ts-node
npx tsc --initOutput:
Wrote to ./package.json: { "name": "graphql-api-demo", "version": "1.0.0", ... }
added 221 packages, and audited 222 packages in 49s
Created a new tsconfig.jsonSetelah instalasi selesai, kita inisialisasi Prisma dan mendefinisikan skema database. Prisma menggunakan file schema.prisma sebagai single source of truth untuk model data.
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}Skema di atas mendefinisikan dua model dengan relasi satu-ke-banyak: seorang User dapat memiliki banyak Post. Field email diberi constraint @unique untuk mencegah duplikasi, sementara field published memiliki nilai default false — post baru tidak otomatis dipublikasikan. Setelah definisi model selesai, kita jalankan migrasi dan generate Prisma Client.
npx prisma migrate dev --name initOutput:
Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"
SQLite database dev.db created at file:./dev.db
Applying migration `20260624011358_init`
The following migration(s) have been created and applied from new schema changes:
prisma/migrations/
└─ 20260624011358_init/
└─ migration.sql
Your database is now in sync with your schema.
✔ Generated Prisma Client (v6.19.3) to ./node_modules/@prisma/clientPerintah ini melakukan dua hal sekaligus: membuat file migrasi SQL yang merepresentasikan perubahan skema, dan mengenerate Prisma Client berdasarkan model yang didefinisikan. Setelah migrasi berhasil, file prisma/schema.prisma akan menjadi referensi utama yang selalu sinkron dengan database.
Sebelum melanjutkan, ada baiknya kita menyiapkan data awal agar resolvers memiliki data untuk diuji.
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const alice = await prisma.user.create({
data: {
name: 'Alice',
email: '[email protected]',
posts: {
create: [
{ title: 'GraphQL 101', content: 'Pengantar GraphQL', published: true },
{ title: 'Prisma Deep Dive', content: 'Mengenal Prisma ORM' },
],
},
},
});
const bob = await prisma.user.create({
data: {
name: 'Bob',
email: '[email protected]',
posts: {
create: [
{ title: 'TypeScript Tips', content: 'Tips TypeScript', published: true },
],
},
},
});
console.log('Seed data created:', { alice, bob });
}
main()
.catch((e) => console.error(e))
.finally(() => prisma.$disconnect());Output:
Seed data created: {
alice: { id: 1, name: 'Alice', email: '[email protected]' },
bob: { id: 2, name: 'Bob', email: '[email protected]' }
}Seed script ini membuat dua user dengan total tiga post. Kita bisa menjalankannya dengan npx ts-node prisma/seed.ts setiap kali database di-reset. Pola ini sangat berguna dalam development dan pipeline CI/CD.
Selanjutnya, kita siapkan Apollo Server yang terintegrasi dengan Prisma Client. Pendekatan yang umum adalah membuat context function yang menyediakan instance Prisma ke semua resolver.
// src/index.ts
import { ApolloServer } from 'apollo-server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({ prisma }),
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
MERN Stack Development
Launch your journey into full-stack web development with this comprehensive, pro...
Dengan pola ini, setiap resolver memiliki akses ke prisma melalui parameter context, tanpa perlu membuat instance baru setiap kali request masuk. Prisma Client di-instantiate sekali di awal dan digunakan kembali sepanjang siklus hidup server.
Designing the GraphQL Schema dengan Type Definitions
GraphQL menggunakan pendekatan schema-first — kita mendefinisikan tipe data dan operasi yang tersedia dalam skema terlebih dahulu, baru kemudian mengimplementasikan resolver-nya. Skema ditulis dalam GraphQL Schema Definition Language (SDL).
// src/schema.ts
export const typeDefs = `
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String
published: Boolean!
author: User!
}
type Query {
users: [User!]!
user(id: ID!): User
posts(published: Boolean, take: Int, skip: Int): [Post!]!
post(id: ID!): Post
}
input CreatePostInput {
title: String!
content: String
authorId: Int!
}
input UpdatePostInput {
title: String
content: String
published: Boolean
}
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post
deletePost(id: ID!): Post
}
`;Perhatikan penggunaan ! untuk non-null constraint dan [Post!]! untuk array yang tidak boleh mengandung elemen null. Tipe User dan Post memiliki relasi timbal balik — ini memungkinkan nested query hingga beberapa level kedalaman. Kita juga mendefinisikan Input types untuk mutation arguments. Penggunaan Input type membuat mutation signature tetap bersih dan mudah diperluas: jika suatu saat kita perlu menambahkan field categoryId ke createPost, cukup tambahkan ke CreatePostInput tanpa mengubah signature mutation.
Bagian Query mendefinisikan entry point untuk pengambilan data, sementara Mutation mendefinisikan operasi tulis. Setiap field di Query dan Mutation akan dipetakan ke resolver function yang sesuai.
Implementing Query Resolvers untuk Fetch Data dengan Prisma
Setelah skema didefinisikan, langkah berikutnya adalah menulis resolver function yang menghubungkan query GraphQL dengan operasi database melalui Prisma.
// src/resolvers.ts
export const resolvers = {
Query: {
users: async (_, __, { prisma }) => {
return prisma.user.findMany();
},
user: async (_, { id }, { prisma }) => {
return prisma.user.findUnique({ where: { id: Number(id) } });
},
posts: async (_, { published, take, skip }, { prisma }) => {
return prisma.post.findMany({
where: published !== undefined ? { published } : undefined,
take: take || undefined,
skip: skip || undefined,
orderBy: { id: 'desc' },
});
},
post: async (_, { id }, { prisma }) => {
return prisma.post.findUnique({ where: { id: Number(id) } });
},
},
User: {
posts: async (parent, _, { prisma }) => {
return prisma.post.findMany({ where: { authorId: parent.id } });
},
},
Post: {
author: async (parent, _, { prisma }) => {
return prisma.user.findUnique({ where: { id: parent.authorId } });
},
},
};Alur kerja resolver cukup sederhana: client mengirim query, Apollo Server mengeksekusi resolver yang sesuai, resolver memanggil Prisma Client untuk mengambil data dari database, dan hasilnya dikembalikan ke client dalam format JSON. Query users akan menjalankan SELECT FROM User, sementara user(id: 1) menghasilkan SELECT FROM User WHERE id = 1.
Resolver untuk relasi (User.posts dan Post.author) memungkinkan client melakukan nested query seperti users { posts { title } } dalam satu request. Ketika client mengirim query seperti itu, Apollo Server pertama-tama mengeksekusi resolver Query.users, kemudian untuk setiap user yang dikembalikan, menjalankan resolver User.posts. Hasilnya digabungkan menjadi satu response JSON dengan struktur persis seperti yang diminta.

Gambar: Ilustrasi resolver chaining di GraphQL — setiap user memicu resolver terpisah untuk mengambil tasks — Sumber: [Dinesh Pandiyan](https://dineshpandiyan.com/blog/graphql-n+1/)
Fitur filtering dan pagination pada query posts menunjukkan fleksibilitas GraphQL. Client bisa meminta hanya post yang sudah dipublikasikan dengan posts(published: true, take: 10, skip: 0), dan server hanya akan menjalankan query database yang sesuai dengan parameter tersebut. Parameter take dan skip mengontrol jumlah data yang dikembalikan — penting untuk mencegah overload pada daftar post yang sangat panjang.
Berikut contoh query yang bisa dijalankan setelah seed data tersedia:
query {
users {
name
email
posts {
title
published
}
}
}Response yang diharapkan adalah array user dengan nested array post masing-masing:
{
"data": {
"users": [
{
"name": "Alice",
"email": "[email protected]",
"posts": [
{ "title": "GraphQL 101", "published": true },
{ "title": "Prisma Deep Dive", "published": false }
]
},
{
"name": "Bob",
"email": "[email protected]",
"posts": [
{ "title": "TypeScript Tips", "published": true }
]
}
]
}
}Output dari eksekusi Apollo Server dengan data seed:
{
"data": {
"users": [
{
"name": "Alice",
"email": "[email protected]",
"posts": [
{ "title": "GraphQL 101", "published": true },
{ "title": "Prisma Deep Dive", "published": false }
]
},
{
"name": "Bob",
"email": "[email protected]",
"posts": [
{ "title": "TypeScript Tips", "published": true }
]
}
]
}
}Perhatikan bahwa GraphQL hanya mengembalikan field yang diminta — tidak ada data berlebih seperti yang terjadi pada REST endpoint yang mengembalikan seluruh record.
Building Mutation Resolvers untuk CRUD Operations
Mutation memungkinkan client mengubah data di server. Berbeda dengan query yang hanya membaca data, mutation biasanya memiliki efek samping dan mengembalikan data yang baru dibuat atau diubah.
// src/resolvers.ts (lanjutan)
export const resolvers = {
Mutation: {
createPost: async (_, { input }, { prisma }) => {
const { title, content, authorId } = input;
const authorExists = await prisma.user.findUnique({
where: { id: authorId },
});
if (!authorExists) {
throw new Error(`User with ID ${authorId} not found`);
}
return prisma.post.create({
data: {
title,
content,
author: { connect: { id: authorId } },
},
});
},
updatePost: async (_, { id, input }, { prisma }) => {
try {
return await prisma.post.update({
where: { id: Number(id) },
data: {
title: input.title !== undefined ? input.title : undefined,
content: input.content !== undefined ? input.content : undefined,
published: input.published !== undefined ? input.published : undefined,
},
});
} catch (error) {
if (error.code === 'P2025') {
throw new Error(`Post with ID ${id} not found`);
}
throw error;
}
},
deletePost: async (_, { id }, { prisma }) => {
try {
return await prisma.post.delete({ where: { id: Number(id) } });
} catch (error) {
if (error.code === 'P2025') {
throw new Error(`Post with ID ${id} not found`);
}
throw error;
}
},
},
};Output dari eksekusi mutation createPost:
{
"data": {
"createPost": {
"id": "4",
"title": "New Post",
"published": false,
"author": {
"name": "Alice"
}
}
}
}Mutation createPost menggunakan connect untuk menghubungkan post baru dengan author yang sudah ada — ini adalah pola standar Prisma untuk membuat relasi. Sebelum membuat post, kita melakukan validasi bahwa author dengan ID yang diberikan benar-benar ada di database. Ini adalah contoh defensive programming: lebih baik memeriksa validitas data lebih awal daripada menunggu constraint violation dari database.
Mutation updatePost dan deletePost dilengkapi dengan error handling yang menangkap Prisma error P2025 (record not found) dan mengembalikan pesan error yang informatif ke client GraphQL. Kode error P2025 adalah kode spesifik Prisma yang menunjukkan operasi gagal karena record yang ditargetkan tidak ditemukan. Dengan menangkap error ini, kita mengubah pesan error yang sebelumnya berbentuk teknis menjadi pesan yang lebih mudah dipahami client.
Untuk error createPost, jika terjadi pelanggaran unique constraint (misalnya, skema diperluas dengan field slug yang unik), Prisma akan melempar error dengan kode P2002. Kita bisa menangkapnya dan memberikan pesan yang relevan, seperti:
if (error.code === 'P2002') {
throw new Error('A record with this value already exists');
}Pendekatan error handling seperti ini memastikan client GraphQL mendapatkan feedback yang jelas dan konsisten ketika operasi gagal, bukan stack trace mentah dari database.
Tackling the N+1 Problem dan Next Steps
Salah satu tantangan terbesar dalam GraphQL adalah N+1 query problem. Masalah ini muncul ketika query seperti { users { posts { title } } } menyebabkan resolver User.posts dijalankan sekali untuk setiap user. Jika ada 100 user, server akan menjalankan 1 query untuk mengambil user + hingga 100 query untuk mengambil posts masing-masing. Pada skala ribuan user, dampaknya terhadap performa sangat signifikan.

Gambar: Visualisasi N+1 data fetching — 1 request untuk users + 4 request untuk tasks masing-masing user — Sumber: [Dinesh Pandiyan](https://dineshpandiyan.com/blog/graphql-n+1/)
Solusi umum adalah DataLoader — library yang melakukan batching dan caching query database. DataLoader mengumpulkan semua request yang terjadi dalam satu event loop cycle, lalu menggabungkannya menjadi satu query dengan IN clause. Berikut implementasi dasarnya:
import DataLoader from 'dataloader';
const createPostsLoader = (prisma: PrismaClient) =>
new DataLoader(async (authorIds: readonly number[]) => {
const posts = await prisma.post.findMany({
where: { authorId: { in: [...authorIds] } },
});
return authorIds.map((id) =>
posts.filter((post) => post.authorId === id)
);
});
// Dalam context Apollo Server:
context: () => ({
prisma,
postsLoader: createPostsLoader(prisma),
}),Dengan DataLoader, 100 request individual untuk posts dari tiap user digabung menjadi satu query SELECT * FROM Post WHERE authorId IN (1, 2, ..., 100). Hasilnya kemudian didistribusikan kembali ke masing-masing resolver berdasarkan authorId. Ini mengurangi jumlah query database dari O(N+1) menjadi hanya 2 query — satu untuk user, satu untuk posts. Efeknya terhadap performa sangat dramatis, terutama pada relasi satu-ke-banyak dengan jumlah data besar.
Untuk production, pertimbangkan juga penambahan authentication guards di resolver menggunakan context. Kita bisa menambahkan informasi user yang sudah diautentikasi ke dalam context Apollo Server, lalu memeriksa otorisasi sebelum menjalankan mutation. Pattern middleware seperti ini memastikan hanya user yang berhak yang bisa mengubah data tertentu.
Setelah dasar-dasar ini dikuasai, langkah selanjutnya yang bisa dieksplorasi meliputi GraphQL Subscriptions untuk real-time data, schema stitching untuk menggabungkan beberapa GraphQL API, dan penggunaan persisted queries untuk optimasi performa.
Kita telah membahas bagaimana GraphQL mengatasi keterbatasan REST, cara mengintegrasikan Apollo Server dengan Prisma, mendesain skema dengan tipe dan relasi, mengimplementasikan query dan mutation resolver, serta memahami tantangan N+1 yang perlu diantisipasi. Pola-pola ini adalah fondasi yang kita gunakan dalam membangun API GraphQL production-ready di track Web Development Bootcamp Rumah Coding, di mana kita juga membahas autentikasi, optimasi performa, dan deployment.
Kursus Terkait
MERN Stack Development
Launch your journey into full-stack web development with this comprehensive, project-driven course. Designed for beginners, this course demystifies the MERN stack (MongoDB, Express.js, React.js, Node.js) by guiding you step-by-step in building a real-world application from scratch. By the end of this course, you will have the practical skills and a complete portfolio project to confidently step into the modern web development industry.
EduStream - Mini Learning Management System (LMS)
- Secure Authentication & Authorization: Robust user registration and login using JWT, with strict role-based access control (Admin/Instructor vs. Student).
- Course Management (Admin Dashboard): Full CRUD (Create, Read, Update, Delete) capabilities for administrators to manage course details, including titles, descriptions, pricing, and thumbnail image uploads.
- Public Course Catalog: An interactive and responsive storefront where users can browse available courses.