UI vs. UX: What’s the difference?

Panduan Dasar GraphQL untuk Pemula

Di era aplikasi modern yang serba cepat, kebutuhan akan pertukaran data yang efisien semakin mendesak. REST API yang selama ini menjadi andalan mulai menunjukkan keterbatasannya — terutama ketika UI membutuhkan data yang kompleks dan bervariasi. Di sinilah GraphQL hadir sebagai solusi.
GraphQL adalah query language sekaligus runtime untuk API yang memungkinkan klien meminta data secara presisi: hanya data yang dibutuhkan, dalam satu permintaan, lewat satu endpoint. Hasilnya? Aplikasi menjadi lebih ringan, respons lebih cepat, dan pengembangan front-end jauh lebih fleksibel.

Dengan schema yang kuat, dokumentasi otomatis, serta dukungan tooling modern, GraphQL tidak hanya mempercepat pengembangan, tapi juga memudahkan evolusi API tanpa harus membuat versi baru. Artikel ini akan membahas konsep inti GraphQL, praktik terbaik, hingga contoh implementasi langsung menggunakan Node.js dan Apollo Server.

1) Apa itu GraphQL (dan kenapa dipakai)?

GraphQL adalah query language untuk API + runtime eksekusi query. Alih-alih banyak endpoint seperti di REST, GraphQL hanya punya satu endpoint yang menerima query terstruktur dari klien: klien memilih data yang dibutuhkan, server mengembalikan tepat data tersebut.

Kelebihan utama:

  • Anti over/under-fetching: klien ambil “pas” yang dibutuhkan.
  • Satu endpoint, bentuk data fleksibel.
  • Tipe data kuat (schema), otomatis dokumentasi & tooling.
  • Evolusi API tanpa versi (versionless).

Kapan cocok:

  • UI kompleks (mobile/web) yang butuh banyak gabungan data.
  • Banyak tim/klien yang konsumsi data berbagai bentuk.
  • Ingin pengembangan cepat dengan tooling (GraphiQL/Apollo).

2) Konsep Inti GraphQL

  • Schema: kontrak tipe data di server.
  • Types: Object, Scalar (Int, Float, String, Boolean, ID), Enum, Input, Interface, Union.
  • Root types: Query (baca), Mutation (tulis), Subscription (real-time).
  • Resolver: fungsi yang “mengisi” tiap field dari schema.
  • SDL (Schema Definition Language): bahasa deklaratif untuk mendefinisikan schema.
  • Variables: parameter dinamis pada query/mutation.
  • Fragments: potongan field yang bisa di-reuse.
  • Directives: aturan runtime seperti @include, @skip.

3) Contoh Schema (SDL)

# Interface umum untuk tipe yang punya id
interface Node { id: ID! }

type User implements Node {
  id: ID!
  name: String!
  email: String!
  posts: [Post!]!
}

type Post implements Node {
  id: ID!
  title: String!
  body: String!
  author: User!
}

# Union untuk hasil pencarian campuran
union SearchResult = User | Post

type Query {
  hello: String
  user(id: ID!): User
  users(limit: Int = 10, offset: Int = 0): [User!]!
  search(term: String!): [SearchResult!]!
}

input CreateUserInput {
  name: String!
  email: String!
}

input UpdateUserInput {
  name: String
  email: String
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
}

type Subscription {
  postAdded: Post!
}

4) Hands-on: Membuat API GraphQL Sederhana (Node.js + Apollo Server)

a) Setup proyek

mkdir graphql-basic && cd graphql-basic
npm init -y
npm i apollo-server graphql

b) Buat index.js

const { ApolloServer, gql } = require('apollo-server');

// 1) Type definitions (SDL)
const typeDefs = gql`
  interface Node { id: ID! }

  type User implements Node {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post implements Node {
    id: ID!
    title: String!
    body: String!
    author: User!
  }

  union SearchResult = User | Post

  type Query {
    hello: String
    user(id: ID!): User
    users(limit: Int = 10, offset: Int = 0): [User!]!
    search(term: String!): [SearchResult!]!
  }

  input CreateUserInput { name: String!, email: String! }
  input UpdateUserInput { name: String, email: String }

  type Mutation {
    createUser(input: CreateUserInput!): User!
    updateUser(id: ID!, input: UpdateUserInput!): User!
    deleteUser(id: ID!): Boolean!
  }
`;

// 2) Mock data (in-memory)
let users = [
  { id: 'u1', name: 'Alice', email: '[email protected]' },
  { id: 'u2', name: 'Bob',   email: '[email protected]'   },
];

let posts = [
  { id: 'p1', title: 'Hello GraphQL', body: 'Intro...', authorId: 'u1' },
  { id: 'p2', title: 'Advanced Tips', body: 'N+1...',   authorId: 'u2' },
];

// 3) Resolvers
const resolvers = {
  Query: {
    hello: () => 'World ????',
    user: (_, { id }) => users.find(u => u.id === id) || null,
    users: (_, { limit, offset }) => users.slice(offset, offset + limit),
    search: (_, { term }) => {
      const byUser = users.filter(u => u.name.toLowerCase().includes(term.toLowerCase()));
      const byPost = posts.filter(p => p.title.toLowerCase().includes(term.toLowerCase()));
      return [...byUser, ...byPost];
    },
  },
  Mutation: {
    createUser: (_, { input }) => {
      const id = `u${users.length + 1}`;
      const user = { id, ...input };
      users.push(user);
      return user;
    },
    updateUser: (_, { id, input }) => {
      const i = users.findIndex(u => u.id === id);
      if (i === -1) throw new Error('User not found');
      users[i] = { ...users[i], ...input };
      return users[i];
    },
    deleteUser: (_, { id }) => {
      const before = users.length;
      users = users.filter(u => u.id !== id);
      return users.length < before;
    }
  },

  // Field-level resolvers (relasi)
  User: {
    posts: (user) => posts.filter(p => p.authorId === user.id),
  },
  Post: {
    author: (post) => users.find(u => u.id === post.authorId),
  },

  // Polymorphism
  SearchResult: {
    __resolveType(obj) {
      if (obj.email) return 'User';
      if (obj.body) return 'Post';
      return null;
    }
  },

  // Interface
  Node: {
    __resolveType(obj) {
      if (obj.email) return 'User';
      if (obj.body) return 'Post';
      return null;
    }
  }
};

// 4) Start server
const server = new ApolloServer({ typeDefs, resolvers });
server.listen().then(({ url }) => console.log(`???? Server ready at ${url}`));

c) Jalankan

node index.js

Buka URL yang muncul (Apollo Sandbox/Playground). Coba query berikut.

5) Contoh Query/Mutation dari Klien

a) Query sederhana

query {
  hello
  users {
    id
    name
    posts { id title }
  }
}

b) Query dengan variables

query GetUser($id: ID!) {
  user(id: $id) { id name email }
}

Variables:

{ "id": "u1" }

c) Fragments untuk reuse field

fragment UserBasic on User { id name }

query {
  users {
    ...UserBasic
    posts { id title }
  }
}

d) Directives kondisional

query Users($withEmail: Boolean!) {
  users {
    id
    name
    email @include(if: $withEmail)
  }
}

Variables:

{ "withEmail": true }

e) Mutations

mutation Create($input: CreateUserInput!) {
  createUser(input: $input) { id name email }
}

Variables:

{
  "input": { "name": "Charlie", "email": "[email protected]" }
}

6) Pagination: Offset vs Cursor

  • Offset/Limit: mudah, cocok untuk dataset kecil. Kekurangan: data bisa “geser” jika ada insert/delete.
  • Cursor/Relay: stabil, cocok skala besar.

Contoh pola Relay:

type PageInfo { hasNextPage: Boolean!, endCursor: String }
type UserEdge { cursor: String!, node: User! }
type UserConnection { edges: [UserEdge!]!, pageInfo: PageInfo! }

type Query {
  usersConnection(first: Int, after: String): UserConnection!
}

7) Error Handling & Partial Data

GraphQL bisa mengembalikan bagian data + errors sekaligus:

{
  "data": { "user": null },
  "errors": [{ "message": "User not found", "path": ["user"] }]
}

Tips:

  • Bedakan error “expected” (validasi) vs “unexpected” (server crash).
  • Jangan bocorkan stack trace di produksi (mask error).
  • Standarisasi kode error di extensions.code jika perlu.

8) N+1 Problem & DataLoader

Saat field relasi dipanggil berulang (mis. author untuk banyak Post), bisa terjadi N+1 queries. Solusi: batching & caching dengan DataLoader.

Gagasan:

  • Kumpulkan semua authorId dari berbagai Post.
  • Ambil sekaligus dengan satu query (batch).
  • Cache hasil untuk request yang sama.

9) Keamanan (Security) Penting

  • Auth & AuthZ: verifikasi user (JWT/session), cek izin di resolver.
  • Depth/Complexity limit: cegah query terlalu dalam/mahal.
  • Rate limiting / persisted queries: cegah abuse.
  • Disable introspection? Umumnya tetap ON untuk dev; di prod boleh ON tapi kombinasikan dengan Auth yang benar.
  • Input validation: validasi di layer schema/input.
  • CORS & CSRF: atur origin tepercaya; gunakan header yang tepat.

10) Praktik Terbaik (Best Practices)

  • Desain schema berbasis use-case, bukan mirror DB.
  • Nama field konsisten, deskriptif, dan camelCase.
  • Wajibkan input melalui input object untuk mutation.
  • Gunakan fragments di klien untuk DRY.
  • Tambahkan deprecation pada field lama (@deprecated).
  • Logging & tracing (Apollo plugins, OpenTelemetry).
  • Dokumentasi deskripsi tipe/field (SDL comments """ ... """).

11) Tooling yang Berguna

  • Server: Apollo Server, GraphQL Yoga, Mercurius (Fastify), Helix.
  • Klien: Apollo Client, urql, Relay.
  • IDE/Testing: GraphiQL/Apollo Sandbox, Insomnia/Postman (GraphQL), graphql-request, Jest + @graphql-tools/mock.
  • Schema: codegen (GraphQL Code Generator), schema registry (Apollo Studio).

12) Menyambungkan dari Frontend (Contoh Apollo Client – React)

# di proyek React
npm i @apollo/client graphql
// apollo.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';

export const client = new ApolloClient({
  link: new HttpLink({ uri: 'http://localhost:4000' }),
  cache: new InMemoryCache(),
});
// App.tsx
import { ApolloProvider, gql, useQuery } from '@apollo/client';
import { client } from './apollo';

const GET_USERS = gql`
  query { users { id name } }
`;

function Users() {
  const { data, loading, error } = useQuery(GET_USERS);
  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;
  return (
    <ul>
      {data.users.map((u: any) => <li key={u.id}>{u.name}</li>)}
    </ul>
  );
}

export default function App() {
  return (
    <ApolloProvider client={client}>
      <Users />
    </ApolloProvider>
  );
}

13) Upload File & Realtime (Gambaran Singkat)

  • Upload file: belum bagian resmi spesifikasi; biasanya via multipart request (graphql-upload, atau rute upload terpisah lalu kirim URL ke mutation).
  • Subscriptions (real-time): GraphQL di atas WebSocket (mis. graphql-ws) untuk event seperti postAdded.

14) Perbandingan Singkat GraphQL vs REST

AspekRESTGraphQL
EndpointBanyakSatu
ResponsBentuk tetapSesuai query klien
Over/Under-fetchUmumMinim
DokumentasiManual/OpenAPIOtomatis dari schema
Evolusi APIVersi (v1, v2)Deprecation & penambahan field

15) Debugging & Observability

  • Aktifkan Playground/Sandbox untuk eksplorasi.
  • Tambahkan logging request, query, variables (hati-hati PII).
  • Tracing resolver (durasi, jumlah panggilan).
  • Monitor error rate & slow query.

16) Checklist Produksi Ringkas

  •  Auth + Authorization per resolver.
  •  Depth/complexity limit.
  •  DataLoader untuk relasi berat.
  •  Masking error & logging terstruktur.
  •  Caching (client) + APQ (persisted queries) jika perlu.
  •  Dokumentasi schema (descriptions).
  •  Deprecation untuk transisi field.
  •  Tests untuk resolver krusial.

17) Latihan Mandiri (Tantangan Mini)

  1. Tambahkan Comment dan relasi Post.comments.
  2. Implementasi pagination cursor pada usersConnection.
  3. Pasang DataLoader untuk Post.author.
  4. Tambahkan Subscription postAdded (gunakan graphql-ws).
  5. Buat directive custom @auth(role: Role!).

18) Kesimpulan

GraphQL memusatkan komunikasi data dalam satu endpoint dengan schema kuat yang bisa dieksplor, membuat front-end lebih lincah dan back-end lebih mudah dievolusi. Mulailah dari schema kecil, tambahkan resolver, jaga keamanan & performa (DataLoader, limits), lalu lengkapi tooling (Apollo/GraphiQL). Setelah nyaman, eksplor topik lanjut seperti cursor pagination, subscriptions, codegen, dan federation.