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 wsimport { 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 dataloaderImplementation
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