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

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: "[email protected]" }, { id: "2", name: "Ani", email: "[email protected]" }, ];

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": "[email protected]", "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) =&gt; {
  if (!context.user || context.user.role !== &quot;ADMIN&quot;) {
    throw new Error(&quot;Not authorized&quot;);
  }
  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(&quot;Email already exists&quot;, {
      extensions: {
        code: &quot;USER_ALREADY_EXISTS&quot;,
        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.