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

Hanya hamba Allah Ta'ala yang berusaha berbuat baik..

Tinggalkan Komentar

Email tidak akan ditampilkan.