Lewati ke konten
Kembali ke Blog

Cara Menggunakan GraphQL untuk API Development

· · 8 menit baca

GraphQL adalah query language untuk API yang memberikan flexibility dalam request data. Mari pelajari cara menggunakannya.

Apa itu GraphQL?

REST vs GraphQL

REST:
- Multiple endpoints
- Over/under fetching
- Fixed response structure
- Versioning dengan URL

GraphQL:
- Single endpoint
- Request exact data needed
- Flexible response
- Schema evolution tanpa versioning

Contoh Comparison

REST - Multiple requests:
GET /users/1
GET /users/1/posts
GET /users/1/followers

GraphQL - Single request:
query {
  user(id: 1) {
    name
    posts {
      title
    }
    followers {
      name
    }
  }
}

Setup GraphQL Server

dengan Node.js dan Apollo Server

# Initialize project
npm init -y

# Install dependencies
npm install @apollo/server graphql
npm install typescript @types/node ts-node --save-dev

# Create tsconfig.json
npx tsc --init

Basic Server

// src/index.ts
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

// Type definitions
const typeDefs = `#graphql
  type User {
    id: ID!
    name: String!
    email: String!
    posts: [Post!]!
  }

  type Post {
    id: ID!
    title: String!
    content: String!
    author: User!
  }

  type Query {
    users: [User!]!
    user(id: ID!): User
    posts: [Post!]!
    post(id: ID!): Post
  }
`;

// Sample data
const users = [
  { id: "1", name: "Budi", email: "budi@example.com" },
  { id: "2", name: "Ani", email: "ani@example.com" },
];

const posts = [
  { id: "1", title: "First Post", content: "Content 1", authorId: "1" },
  { id: "2", title: "Second Post", content: "Content 2", authorId: "1" },
  { id: "3", title: "Third Post", content: "Content 3", authorId: "2" },
];

// Resolvers
const resolvers = {
  Query: {
    users: () => users,
    user: (_: any, args: { id: string }) =>
      users.find((user) => user.id === args.id),
    posts: () => posts,
    post: (_: any, args: { id: string }) =>
      posts.find((post) => post.id === args.id),
  },
  User: {
    posts: (parent: { id: string }) =>
      posts.filter((post) => post.authorId === parent.id),
  },
  Post: {
    author: (parent: { authorId: string }) =>
      users.find((user) => user.id === parent.authorId),
  },
};

// Start server
const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const startServer = async () => {
  const { url } = await startStandaloneServer(server, {
    listen: { port: 4000 },
  });
  console.log(`🚀 Server ready at: ${url}`);
};

startServer();

Schema Definition

Scalar Types

# Built-in scalar types
type Example {
  id: ID! # Unique identifier
  name: String! # UTF-8 string
  age: Int # 32-bit integer
  price: Float # Double-precision float
  active: Boolean # true/false
}

# Custom scalar
scalar DateTime
scalar JSON

Object Types

type User {
  id: ID!
  name: String!
  email: String!
  createdAt: DateTime!
  profile: Profile
  posts: [Post!]!
}

type Profile {
  bio: String
  avatar: String
  website: String
}

type Post {
  id: ID!
  title: String!
  content: String!
  published: Boolean!
  author: User!
  tags: [String!]!
  comments: [Comment!]!
}

type Comment {
  id: ID!
  text: String!
  author: User!
  post: Post!
}

Input Types

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

input UpdateUserInput {
  name: String
  email: String
  bio: String
}

input PostFilterInput {
  published: Boolean
  authorId: ID
  tag: String
}

Enums

enum Role {
  USER
  ADMIN
  MODERATOR
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

type User {
  id: ID!
  name: String!
  role: Role!
}

Interfaces

interface Node {
  id: ID!
}

interface Timestamped {
  createdAt: DateTime!
  updatedAt: DateTime!
}

type User implements Node & Timestamped {
  id: ID!
  name: String!
  createdAt: DateTime!
  updatedAt: DateTime!
}

Unions

union SearchResult = User | Post | Comment

type Query {
  search(term: String!): [SearchResult!]!
}

Queries

Basic Query

# Schema
type Query {
  users: [User!]!
  user(id: ID!): User
  posts(filter: PostFilterInput, limit: Int, offset: Int): [Post!]!
}

# Client query
query GetUser {
  user(id: "1") {
    name
    email
    posts {
      title
    }
  }
}

# Query with variables
query GetUser($userId: ID!) {
  user(id: $userId) {
    name
    email
  }
}

# Variables
{
  "userId": "1"
}

Query with Arguments

# Schema
type Query {
  posts(
    limit: Int = 10
    offset: Int = 0
    published: Boolean
    orderBy: PostOrderBy
  ): [Post!]!
}

enum PostOrderBy {
  CREATED_AT_ASC
  CREATED_AT_DESC
  TITLE_ASC
  TITLE_DESC
}

# Client query
query GetPosts {
  posts(limit: 5, published: true, orderBy: CREATED_AT_DESC) {
    id
    title
    createdAt
  }
}

Fragments

# Define fragment
fragment UserBasicInfo on User {
  id
  name
  email
}

fragment PostInfo on Post {
  id
  title
  content
  createdAt
}

# Use fragments
query GetData {
  user(id: "1") {
    ...UserBasicInfo
    posts {
      ...PostInfo
    }
  }
}

Aliases

query GetMultipleUsers {
  firstUser: user(id: "1") {
    name
  }
  secondUser: user(id: "2") {
    name
  }
}

Mutations

Basic Mutations

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

  createPost(input: CreatePostInput!): Post!
  publishPost(id: ID!): Post
  deletePost(id: ID!): Boolean!
}

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

input CreatePostInput {
  title: String!
  content: String!
  published: Boolean = false
}

# Client mutation
mutation CreateUser($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
    email
  }
}

# Variables
{
  "input": {
    "name": "Budi",
    "email": "budi@example.com",
    "password": "secret123"
  }
}

Resolver Implementation

const resolvers = {
  Mutation: {
    createUser: async (_: any, { input }: { input: CreateUserInput }) => {
      const user = await db.user.create({
        data: {
          name: input.name,
          email: input.email,
          password: await hashPassword(input.password),
        },
      });
      return user;
    },

    updateUser: async (
      _: any,
      { id, input }: { id: string; input: UpdateUserInput }
    ) => {
      const user = await db.user.update({
        where: { id },
        data: input,
      });
      return user;
    },

    deleteUser: async (_: any, { id }: { id: string }) => {
      await db.user.delete({ where: { id } });
      return true;
    },
  },
};

Subscriptions

Setup Subscriptions

// Install additional dependencies
// npm install graphql-ws ws

import { createServer } from "http";
import { WebSocketServer } from "ws";
import { useServer } from "graphql-ws/lib/use/ws";
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import { makeExecutableSchema } from "@graphql-tools/schema";
import { PubSub } from "graphql-subscriptions";

const pubsub = new PubSub();

const typeDefs = `#graphql
  type Subscription {
    postCreated: Post!
    commentAdded(postId: ID!): Comment!
  }
`;

const resolvers = {
  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(["POST_CREATED"]),
    },
    commentAdded: {
      subscribe: (_: any, { postId }: { postId: string }) => {
        return pubsub.asyncIterator([`COMMENT_ADDED_${postId}`]);
      },
    },
  },
  Mutation: {
    createPost: async (_: any, { input }: { input: CreatePostInput }) => {
      const post = await db.post.create({ data: input });
      pubsub.publish("POST_CREATED", { postCreated: post });
      return post;
    },
  },
};

Client Subscription

subscription OnPostCreated {
  postCreated {
    id
    title
    author {
      name
    }
  }
}

Authentication

Context Setup

import { ApolloServer } from "@apollo/server";
import jwt from "jsonwebtoken";

interface Context {
  user?: {
    id: string;
    role: string;
  };
}

const server = new ApolloServer<Context>({
  typeDefs,
  resolvers,
});

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    const token = req.headers.authorization?.replace("Bearer ", "");

    if (token) {
      try {
        const user = jwt.verify(token, process.env.JWT_SECRET!) as {
          id: string;
          role: string;
        };
        return { user };
      } catch {
        return {};
      }
    }

    return {};
  },
  listen: { port: 4000 },
});

Protected Resolvers

const resolvers = {
  Query: {
    me: (_: any, __: any, context: Context) => {
      if (!context.user) {
        throw new Error("Not authenticated");
      }
      return db.user.findUnique({ where: { id: context.user.id } });
    },
  },

  Mutation: {
    createPost: (_: any, { input }: any, context: Context) => {
      if (!context.user) {
        throw new Error("Not authenticated");
      }
      return db.post.create({
        data: {
          ...input,
          authorId: context.user.id,
        },
      });
    },

    deleteUser: (_: any, { id }: { id: string }, context: Context) => {
      if (!context.user || context.user.role !== "ADMIN") {
        throw new Error("Not authorized");
      }
      return db.user.delete({ where: { id } });
    },
  },
};

Error Handling

Custom Errors

import { GraphQLError } from "graphql";

const resolvers = {
  Mutation: {
    createUser: async (_: any, { input }: any) => {
      const existingUser = await db.user.findUnique({
        where: { email: input.email },
      });

      if (existingUser) {
        throw new GraphQLError("Email already exists", {
          extensions: {
            code: "USER_ALREADY_EXISTS",
            http: { status: 400 },
          },
        });
      }

      return db.user.create({ data: input });
    },
  },
};

Error Response

{
  "errors": [
    {
      "message": "Email already exists",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["createUser"],
      "extensions": {
        "code": "USER_ALREADY_EXISTS"
      }
    }
  ],
  "data": null
}

DataLoader (N+1 Problem)

Install DataLoader

npm install dataloader

Implementation

import DataLoader from "dataloader";

// Batch function
const batchUsers = async (userIds: readonly string[]) => {
  const users = await db.user.findMany({
    where: { id: { in: [...userIds] } },
  });

  // Return dalam order yang sama dengan input
  const userMap = new Map(users.map((u) => [u.id, u]));
  return userIds.map((id) => userMap.get(id) || null);
};

// Create loader per request
const context = async ({ req }: any) => ({
  loaders: {
    userLoader: new DataLoader(batchUsers),
  },
});

// Use in resolver
const resolvers = {
  Post: {
    author: (parent: any, _: any, context: any) => {
      return context.loaders.userLoader.load(parent.authorId);
    },
  },
};

Best Practices

Schema Design

# Use meaningful names
type User {
  # Good
  createdAt: DateTime!
  updatedAt: DateTime!

  # Avoid
  created: String!
  updated: String!
}

# Nullable by default, use ! for required
type Post {
  id: ID! # Required
  title: String! # Required
  subtitle: String # Optional
}

# Use Input types untuk mutations
input CreateUserInput {
  name: String!
  email: String!
}

# Return object dari mutation (bukan Boolean)
type Mutation {
  # Good
  createUser(input: CreateUserInput!): User!

  # Avoid
  createUser(name: String!, email: String!): Boolean!
}

Performance

// Use DataLoader
// Implement pagination
// Use query complexity analysis
// Cache when appropriate

// Pagination
type Query {
  posts(
    first: Int
    after: String
    last: Int
    before: String
  ): PostConnection!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
}

type PostEdge {
  cursor: String!
  node: Post!
}

type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

Kesimpulan

GraphQL memberikan flexibility dan efficiency dalam API development. Ideal untuk complex data requirements dengan multiple interconnected resources.

Ditulis oleh

Hendra Wijaya

Tinggalkan Komentar

Email tidak akan ditampilkan.