GraphQL API dengan Apollo Server dan Prisma: Query dan Mutation untuk Aplikasi Modern

Lhuqita Fazry
Web Development GraphQL Apollo Server Prisma Node.js
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.

Perbandingan arsitektur REST vs GraphQL — REST membutuhkan multiple round-trip, GraphQL cukup satu request

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.

bashbash
mkdir graphql-api-demo && cd graphql-api-demo
npm init -y
npm install apollo-server prisma @prisma/client typescript ts-node
npx tsc --init

Output:

text
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.json

Setelah instalasi selesai, kita inisialisasi Prisma dan mendefinisikan skema database. Prisma menggunakan file schema.prisma sebagai single source of truth untuk model data.

typescripttypescript
// 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.

bashbash
npx prisma migrate dev --name init

Output:

text
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/client

Perintah 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.

typescripttypescript
// 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:

text
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.

typescripttypescript
// 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
Web App • Beginner

MERN Stack Development

Launch your journey into full-stack web development with this comprehensive, pro...

Daftar

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).

typescripttypescript
// 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.

typescripttypescript
// 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.

Resolver chaining di GraphQL — setiap field di parent memicu resolver terpisah untuk data relasi

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:

graphqlgraphql
query {
  users {
    name
    email
    posts {
      title
      published
    }
  }
}

Response yang diharapkan adalah array user dengan nested array post masing-masing:

jsonjson
{
  "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:

text
{
    "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.

typescripttypescript
// 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:

text
{
    "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:

typescripttypescript
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.

Visualisasi N+1 problem di GraphQL — satu query awal memicu N query tambahan

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:

typescripttypescript
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

EduStream - Mini Learning Management System (LMS)
Kursus Premium Web App

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.

Proyek Akhir

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.
7 Weeks Beginner
Lihat Detail Kursus

Artikel Terkait