GraphQL API Development: Panduan Lengkap dari Basic hingga Advanced
GraphQL telah mengubah cara kita membangun dan mengonsumsi API. Artikel ini akan membahas secara mendalam bagaimana membangun GraphQL API yang efisien, scalable, dan maintainable.
Apa itu GraphQL?
GraphQL adalah query language dan runtime untuk API yang dikembangkan oleh Facebook. Berbeda dengan REST API, GraphQL memungkinkan client untuk meminta data yang spesifik sesuai kebutuhan.
GraphQL vs REST API
| Aspect | GraphQL | REST | | ------------------- | ----------------------------- | ---------------------------------- | | Data Fetching | Single request, specific data | Multiple requests, fixed structure | | Over/Under-fetching | Eliminated | Common problem | | Versioning | Schema evolution | URL versioning | | Caching | Complex | Simple (HTTP caching) | | Learning Curve | Steep | Gentle | | Tooling | Rich ecosystem | Mature ecosystem |
Keuntungan GraphQL
- Efficient Data Loading: Hanya fetch data yang dibutuhkan
- Strong Type System: Schema-first development
- Single Endpoint: Satu URL untuk semua operations
- Real-time Subscriptions: Built-in support untuk real-time data
- Introspection: Self-documenting API
- Developer Experience: Excellent tooling dan debugging
Core Concepts
1. Schema Definition Language (SDL)
# User type definition
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: DateTime!
}
# Post type definition
type Post {
id: ID!
title: String!
content: String!
author: User!
tags: [String!]!
published: Boolean!
createdAt: DateTime!
updatedAt: DateTime!
}
# Custom scalar
scalar DateTime
# Input types
input CreateUserInput {
name: String!
email: String!
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
published: Boolean
}
# Root types
type Query {
# Get single user
user(id: ID!): User
# Get users with pagination
users(first: Int, after: String): UserConnection!
# Get posts with filtering
posts(
first: Int
after: String
filter: PostFilter
orderBy: PostOrderBy
): PostConnection!
# Search posts
searchPosts(query: String!): [Post!]!
}
type Mutation {
# User mutations
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
# Post mutations
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
# Bulk operations
publishPosts(ids: [ID!]!): [Post!]!
}
type Subscription {
# Real-time updates
postAdded: Post!
postUpdated(id: ID!): Post!
userOnline: User!
}
# Pagination types
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Filter and ordering
input PostFilter {
published: Boolean
authorId: ID
tags: [String!]
createdAfter: DateTime
createdBefore: DateTime
}
enum PostOrderBy {
CREATED_AT_ASC
CREATED_AT_DESC
TITLE_ASC
TITLE_DESC
UPDATED_AT_ASC
UPDATED_AT_DESC
}
2. Resolvers
Resolvers adalah functions yang mengambil data untuk setiap field dalam schema.
// resolvers.js
const { PubSub } = require("graphql-subscriptions");
const pubsub = new PubSub();
const resolvers = {
// Scalar resolvers
DateTime: {
serialize: (date) => date.toISOString(),
parseValue: (value) => new Date(value),
parseLiteral: (ast) => new Date(ast.value),
},
// Query resolvers
Query: {
user: async (parent, { id }, context) => {
return await context.dataSources.userAPI.getUserById(id);
},
users: async (parent, { first = 10, after }, context) => {
const result = await context.dataSources.userAPI.getUsers({
first,
after,
});
return {
edges: result.users.map((user) => ({
node: user,
cursor: Buffer.from(user.id).toString("base64"),
})),
pageInfo: {
hasNextPage: result.hasNextPage,
hasPreviousPage: result.hasPreviousPage,
startCursor:
result.users.length > 0
? Buffer.from(result.users[0].id).toString("base64")
: null,
endCursor:
result.users.length > 0
? Buffer.from(result.users[result.users.length - 1].id).toString(
"base64"
)
: null,
},
totalCount: result.totalCount,
};
},
posts: async (parent, { first = 10, after, filter, orderBy }, context) => {
return await context.dataSources.postAPI.getPosts({
first,
after,
filter,
orderBy,
});
},
searchPosts: async (parent, { query }, context) => {
return await context.dataSources.searchAPI.searchPosts(query);
},
},
// Mutation resolvers
Mutation: {
createUser: async (parent, { input }, context) => {
// Validate input
await context.validators.validateCreateUser(input);
// Check permissions
context.auth.requirePermission("CREATE_USER");
// Create user
const user = await context.dataSources.userAPI.createUser(input);
// Publish subscription
pubsub.publish("USER_CREATED", { userCreated: user });
return user;
},
updateUser: async (parent, { id, input }, context) => {
// Check ownership or admin permission
const currentUser = context.auth.getCurrentUser();
if (currentUser.id !== id && !currentUser.isAdmin) {
throw new Error("Unauthorized");
}
const user = await context.dataSources.userAPI.updateUser(id, input);
pubsub.publish("USER_UPDATED", { userUpdated: user });
return user;
},
createPost: async (parent, { input }, context) => {
const currentUser = context.auth.getCurrentUser();
const post = await context.dataSources.postAPI.createPost({
...input,
authorId: currentUser.id,
});
pubsub.publish("POST_ADDED", { postAdded: post });
return post;
},
publishPosts: async (parent, { ids }, context) => {
context.auth.requirePermission("PUBLISH_POSTS");
const posts = await context.dataSources.postAPI.publishPosts(ids);
// Publish individual updates
posts.forEach((post) => {
pubsub.publish("POST_UPDATED", { postUpdated: post });
});
return posts;
},
},
// Subscription resolvers
Subscription: {
postAdded: {
subscribe: () => pubsub.asyncIterator(["POST_ADDED"]),
},
postUpdated: {
subscribe: (parent, { id }) => {
return pubsub.asyncIterator([`POST_UPDATED_${id}`]);
},
},
userOnline: {
subscribe: withFilter(
() => pubsub.asyncIterator(["USER_ONLINE"]),
(payload, variables, context) => {
// Filter based on user permissions
return context.auth.canSeeUserStatus(payload.userOnline.id);
}
),
},
},
// Type resolvers
User: {
posts: async (parent, args, context) => {
// DataLoader untuk menghindari N+1 problem
return await context.loaders.postsByUserId.load(parent.id);
},
},
Post: {
author: async (parent, args, context) => {
return await context.loaders.userById.load(parent.authorId);
},
},
};
module.exports = resolvers;
Server Setup dengan Apollo Server
1. Basic Setup
// server.js
const { ApolloServer } = require("apollo-server-express");
const { makeExecutableSchema } = require("@graphql-tools/schema");
const express = require("express");
const { createServer } = require("http");
const { SubscriptionServer } = require("subscriptions-transport-ws");
const { execute, subscribe } = require("graphql");
const typeDefs = require("./schema");
const resolvers = require("./resolvers");
const { createDataSources } = require("./dataSources");
const { createLoaders } = require("./loaders");
const { authMiddleware } = require("./middleware/auth");
async function startServer() {
const app = express();
const httpServer = createServer(app);
// Create executable schema
const schema = makeExecutableSchema({
typeDefs,
resolvers,
});
// Create Apollo Server
const server = new ApolloServer({
schema,
context: async ({ req, connection }) => {
// WebSocket connection (subscriptions)
if (connection) {
return {
...connection.context,
dataSources: createDataSources(),
loaders: createLoaders(),
};
}
// HTTP request
const auth = await authMiddleware(req);
return {
auth,
dataSources: createDataSources(),
loaders: createLoaders(),
req,
};
},
// Error formatting
formatError: (error) => {
console.error("GraphQL Error:", error);
// Don't expose internal errors in production
if (process.env.NODE_ENV === "production") {
if (error.message.startsWith("Database")) {
return new Error("Internal server error");
}
}
return error;
},
// Enable GraphQL Playground in development
introspection: process.env.NODE_ENV !== "production",
playground: process.env.NODE_ENV !== "production",
});
await server.start();
server.applyMiddleware({ app, path: "/graphql" });
// Setup subscription server
const subscriptionServer = SubscriptionServer.create(
{
schema,
execute,
subscribe,
onConnect: async (connectionParams, webSocket) => {
// Authenticate WebSocket connection
const auth = await authMiddleware({
headers: connectionParams,
});
return { auth };
},
onDisconnect: (webSocket, context) => {
console.log("Client disconnected");
},
},
{
server: httpServer,
path: "/graphql",
}
);
const PORT = process.env.PORT || 4000;
httpServer.listen(PORT, () => {
console.log(
`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`
);
console.log(
`🚀 Subscriptions ready at ws://localhost:${PORT}${server.graphqlPath}`
);
});
}
startServer().catch((error) => {
console.error("Error starting server:", error);
});
2. Data Sources
// dataSources/userAPI.js
const { DataSource } = require("apollo-datasource");
const { UserInputError } = require("apollo-server-express");
class UserAPI extends DataSource {
constructor() {
super();
}
initialize(config) {
this.context = config.context;
this.db = config.context.db;
}
async getUserById(id) {
const user = await this.db.user.findUnique({
where: { id },
include: {
posts: true,
},
});
if (!user) {
throw new UserInputError("User not found");
}
return user;
}
async getUsers({ first, after }) {
const cursor = after
? { id: Buffer.from(after, "base64").toString() }
: undefined;
const users = await this.db.user.findMany({
take: first + 1, // +1 to check if there's next page
cursor: cursor ? { id: cursor.id } : undefined,
skip: cursor ? 1 : 0,
orderBy: { createdAt: "desc" },
});
const hasNextPage = users.length > first;
const nodes = hasNextPage ? users.slice(0, -1) : users;
const totalCount = await this.db.user.count();
return {
users: nodes,
hasNextPage,
hasPreviousPage: !!cursor,
totalCount,
};
}
async createUser(input) {
// Validate email uniqueness
const existingUser = await this.db.user.findUnique({
where: { email: input.email },
});
if (existingUser) {
throw new UserInputError("Email already exists");
}
return await this.db.user.create({
data: input,
});
}
async updateUser(id, input) {
return await this.db.user.update({
where: { id },
data: input,
});
}
}
module.exports = UserAPI;
3. DataLoaders untuk N+1 Problem
// loaders/index.js
const DataLoader = require("dataloader");
const { db } = require("../database");
function createLoaders() {
return {
// User loader
userById: new DataLoader(async (ids) => {
const users = await db.user.findMany({
where: { id: { in: ids } },
});
// Return users in same order as requested IDs
return ids.map((id) => users.find((user) => user.id === id));
}),
// Posts by user ID
postsByUserId: new DataLoader(async (userIds) => {
const posts = await db.post.findMany({
where: { authorId: { in: userIds } },
orderBy: { createdAt: "desc" },
});
// Group posts by user ID
const postsByUser = userIds.map((userId) =>
posts.filter((post) => post.authorId === userId)
);
return postsByUser;
}),
// Comments by post ID
commentsByPostId: new DataLoader(async (postIds) => {
const comments = await db.comment.findMany({
where: { postId: { in: postIds } },
include: { author: true },
orderBy: { createdAt: "asc" },
});
return postIds.map((postId) =>
comments.filter((comment) => comment.postId === postId)
);
}),
};
}
module.exports = { createLoaders };
Advanced Features
1. Custom Directives
// directives/auth.js
const { SchemaDirectiveVisitor } = require("apollo-server-express");
const { defaultFieldResolver } = require("graphql");
const {
AuthenticationError,
ForbiddenError,
} = require("apollo-server-express");
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const requiredRole = this.args.requires;
field.resolve = async function (...args) {
const [, , context] = args;
if (!context.auth.user) {
throw new AuthenticationError("You must be logged in");
}
if (requiredRole && !context.auth.user.roles.includes(requiredRole)) {
throw new ForbiddenError("Insufficient permissions");
}
return resolve.apply(this, args);
};
}
}
class RateLimitDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const { max, window } = this.args;
field.resolve = async function (...args) {
const [, , context] = args;
const key = `${context.auth.user?.id || context.req.ip}:${field.name}`;
const current = await context.redis.incr(key);
if (current === 1) {
await context.redis.expire(key, window);
}
if (current > max) {
throw new Error("Rate limit exceeded");
}
return resolve.apply(this, args);
};
}
}
module.exports = {
auth: AuthDirective,
rateLimit: RateLimitDirective,
};
# Schema with directives
directive @auth(requires: Role = USER) on FIELD_DEFINITION
directive @rateLimit(max: Int!, window: Int!) on FIELD_DEFINITION
enum Role {
USER
ADMIN
MODERATOR
}
type Mutation {
createPost(input: CreatePostInput!): Post!
@auth(requires: USER)
@rateLimit(max: 5, window: 60)
deleteUser(id: ID!): Boolean! @auth(requires: ADMIN)
banUser(id: ID!): Boolean! @auth(requires: MODERATOR)
}
2. File Upload
// File upload resolver
const { GraphQLUpload } = require("graphql-upload");
const { createWriteStream } = require("fs");
const { join } = require("path");
const { v4: uuid } = require("uuid");
const resolvers = {
Upload: GraphQLUpload,
Mutation: {
uploadFile: async (parent, { file }, context) => {
const { createReadStream, filename, mimetype, encoding } = await file;
// Validate file type
const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
if (!allowedTypes.includes(mimetype)) {
throw new Error("Invalid file type");
}
// Generate unique filename
const extension = filename.split(".").pop();
const uniqueFilename = `${uuid()}.${extension}`;
const filepath = join(__dirname, "../uploads", uniqueFilename);
// Save file
const stream = createReadStream();
await new Promise((resolve, reject) => {
stream
.pipe(createWriteStream(filepath))
.on("finish", resolve)
.on("error", reject);
});
// Save file info to database
const fileRecord = await context.db.file.create({
data: {
filename: uniqueFilename,
originalName: filename,
mimetype,
size: stream.bytesRead,
path: filepath,
uploadedBy: context.auth.user.id,
},
});
return {
id: fileRecord.id,
filename: uniqueFilename,
url: `/uploads/${uniqueFilename}`,
};
},
},
};
3. Real-time Subscriptions
// Advanced subscription with filtering
const { withFilter } = require("graphql-subscriptions");
const { RedisPubSub } = require("graphql-redis-subscriptions");
const Redis = require("ioredis");
const redis = new Redis(process.env.REDIS_URL);
const pubsub = new RedisPubSub({
publisher: redis,
subscriber: redis,
});
const resolvers = {
Subscription: {
messageAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator(["MESSAGE_ADDED"]),
(payload, variables, context) => {
// Only send to users in the same room
return (
payload.messageAdded.roomId === variables.roomId &&
context.auth.user.rooms.includes(variables.roomId)
);
}
),
},
notificationAdded: {
subscribe: withFilter(
() => pubsub.asyncIterator(["NOTIFICATION_ADDED"]),
(payload, variables, context) => {
// Only send to the target user
return payload.notificationAdded.userId === context.auth.user.id;
}
),
},
postUpdated: {
subscribe: (parent, { id }, context) => {
// Subscribe to specific post updates
return pubsub.asyncIterator([`POST_UPDATED_${id}`]);
},
},
},
};
Testing GraphQL APIs
1. Unit Testing Resolvers
// tests/resolvers/user.test.js
const { createTestClient } = require("apollo-server-testing");
const { ApolloServer } = require("apollo-server-express");
const typeDefs = require("../../schema");
const resolvers = require("../../resolvers");
describe("User Resolvers", () => {
let server, query, mutate;
beforeEach(() => {
server = new ApolloServer({
typeDefs,
resolvers,
context: () => ({
auth: {
user: { id: "1", email: "test@example.com" },
requirePermission: jest.fn(),
},
dataSources: {
userAPI: {
getUserById: jest.fn(),
createUser: jest.fn(),
updateUser: jest.fn(),
},
},
loaders: {
userById: {
load: jest.fn(),
},
},
}),
});
const testClient = createTestClient(server);
query = testClient.query;
mutate = testClient.mutate;
});
describe("Query.user", () => {
it("should return user by ID", async () => {
const mockUser = {
id: "1",
name: "John Doe",
email: "john@example.com",
};
server.requestOptions.context.dataSources.userAPI.getUserById.mockResolvedValue(
mockUser
);
const GET_USER = `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`;
const { data } = await query({
query: GET_USER,
variables: { id: "1" },
});
expect(data.user).toEqual(mockUser);
});
});
describe("Mutation.createUser", () => {
it("should create new user", async () => {
const input = {
name: "Jane Doe",
email: "jane@example.com",
};
const mockUser = {
id: "2",
...input,
};
server.requestOptions.context.dataSources.userAPI.createUser.mockResolvedValue(
mockUser
);
const CREATE_USER = `
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`;
const { data } = await mutate({
mutation: CREATE_USER,
variables: { input },
});
expect(data.createUser).toEqual(mockUser);
});
});
});
2. Integration Testing
// tests/integration/api.test.js
const request = require("supertest");
const { createTestServer } = require("../helpers/testServer");
const { seedDatabase, cleanDatabase } = require("../helpers/database");
describe("GraphQL API Integration", () => {
let app, server;
beforeAll(async () => {
({ app, server } = await createTestServer());
await seedDatabase();
});
afterAll(async () => {
await cleanDatabase();
await server.close();
});
describe("User Operations", () => {
it("should create and retrieve user", async () => {
// Create user
const createMutation = `
mutation {
createUser(input: {
name: "Test User"
email: "test@example.com"
}) {
id
name
email
}
}
`;
const createResponse = await request(app)
.post("/graphql")
.send({ query: createMutation })
.expect(200);
const userId = createResponse.body.data.createUser.id;
// Retrieve user
const getQuery = `
query {
user(id: "${userId}") {
id
name
email
}
}
`;
const getResponse = await request(app)
.post("/graphql")
.send({ query: getQuery })
.expect(200);
expect(getResponse.body.data.user).toMatchObject({
id: userId,
name: "Test User",
email: "test@example.com",
});
});
});
});
Performance Optimization
1. Query Complexity Analysis
// middleware/queryComplexity.js
const { createComplexityLimitRule } = require("graphql-query-complexity");
const { ValidationContext } = require("graphql");
const complexityLimitRule = createComplexityLimitRule(1000, {
maximumComplexity: 1000,
variables: {},
createError: (max, actual) => {
return new Error(
`Query complexity ${actual} exceeds maximum complexity ${max}`
);
},
onComplete: (complexity) => {
console.log("Query complexity:", complexity);
},
});
// In Apollo Server
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [complexityLimitRule],
});
2. Query Depth Limiting
// middleware/depthLimit.js
const depthLimit = require("graphql-depth-limit");
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [depthLimit(10)], // Max depth 10
});
3. Caching Strategies
// caching/redis.js
const Redis = require("ioredis");
const redis = new Redis(process.env.REDIS_URL);
class CacheAPI {
async get(key) {
const cached = await redis.get(key);
return cached ? JSON.parse(cached) : null;
}
async set(key, value, ttl = 3600) {
await redis.setex(key, ttl, JSON.stringify(value));
}
async del(key) {
await redis.del(key);
}
async invalidatePattern(pattern) {
const keys = await redis.keys(pattern);
if (keys.length > 0) {
await redis.del(...keys);
}
}
}
// In resolvers
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
const cacheKey = `user:${id}`;
// Try cache first
let user = await context.cache.get(cacheKey);
if (!user) {
// Fetch from database
user = await context.dataSources.userAPI.getUserById(id);
// Cache for 1 hour
await context.cache.set(cacheKey, user, 3600);
}
return user;
},
},
Mutation: {
updateUser: async (parent, { id, input }, context) => {
const user = await context.dataSources.userAPI.updateUser(id, input);
// Invalidate cache
await context.cache.del(`user:${id}`);
await context.cache.invalidatePattern(`users:*`);
return user;
},
},
};
Security Best Practices
1. Authentication & Authorization
// middleware/auth.js
const jwt = require("jsonwebtoken");
const { AuthenticationError } = require("apollo-server-express");
class AuthService {
constructor(user, permissions = []) {
this.user = user;
this.permissions = permissions;
}
requireAuth() {
if (!this.user) {
throw new AuthenticationError("Authentication required");
}
}
requirePermission(permission) {
this.requireAuth();
if (!this.permissions.includes(permission)) {
throw new ForbiddenError(`Permission ${permission} required`);
}
}
canAccess(resource) {
this.requireAuth();
// Resource-based access control
if (resource.ownerId === this.user.id) {
return true;
}
if (this.user.role === "ADMIN") {
return true;
}
return false;
}
}
async function authMiddleware(req) {
const token = req.headers.authorization?.replace("Bearer ", "");
if (!token) {
return new AuthService(null);
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await getUserById(decoded.userId);
const permissions = await getUserPermissions(user.id);
return new AuthService(user, permissions);
} catch (error) {
throw new AuthenticationError("Invalid token");
}
}
2. Input Validation
// validation/schemas.js
const Joi = require("joi");
const schemas = {
createUser: Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string()
.min(8)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.required(),
}),
updatePost: Joi.object({
title: Joi.string().min(5).max(200),
content: Joi.string().min(10),
tags: Joi.array().items(Joi.string().max(20)).max(10),
}),
};
class ValidationService {
async validateCreateUser(input) {
const { error } = schemas.createUser.validate(input);
if (error) {
throw new UserInputError(error.details[0].message);
}
}
async validateUpdatePost(input) {
const { error } = schemas.updatePost.validate(input);
if (error) {
throw new UserInputError(error.details[0].message);
}
}
}
3. Rate Limiting
// middleware/rateLimiting.js
const { RateLimiterRedis } = require("rate-limiter-flexible");
const Redis = require("ioredis");
const redis = new Redis(process.env.REDIS_URL);
const rateLimiters = {
// General API rate limiting
general: new RateLimiterRedis({
storeClient: redis,
keyPrefix: "rl_general",
points: 100, // requests
duration: 60, // per 60 seconds
}),
// Mutation rate limiting
mutation: new RateLimiterRedis({
storeClient: redis,
keyPrefix: "rl_mutation",
points: 10,
duration: 60,
}),
// Login attempts
login: new RateLimiterRedis({
storeClient: redis,
keyPrefix: "rl_login",
points: 5,
duration: 900, // 15 minutes
blockDuration: 900,
}),
};
function createRateLimitMiddleware(limiterName) {
return async (resolve, parent, args, context, info) => {
const key = context.auth.user?.id || context.req.ip;
try {
await rateLimiters[limiterName].consume(key);
return resolve(parent, args, context, info);
} catch (rejRes) {
throw new Error(
`Rate limit exceeded. Try again in ${rejRes.msBeforeNext}ms`
);
}
};
}
Monitoring dan Observability
1. Metrics Collection
// monitoring/metrics.js
const { ApolloServerPluginUsageReporting } = require("apollo-server-core");
const { createPrometheusMetricsPlugin } = require("apollo-prometheus-plugin");
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
// Apollo Studio reporting
ApolloServerPluginUsageReporting({
apiKey: process.env.APOLLO_KEY,
sendVariableValues: { all: true },
sendHeaders: { all: true },
}),
// Prometheus metrics
createPrometheusMetricsPlugin({
registry: require("prom-client").register,
metrics: {
graphql_request_duration_seconds: true,
graphql_request_total: true,
graphql_resolver_duration_seconds: true,
},
}),
// Custom logging plugin
{
requestDidStart() {
return {
didResolveOperation(requestContext) {
console.log("Operation:", requestContext.request.operationName);
},
didEncounterErrors(requestContext) {
console.error("GraphQL errors:", requestContext.errors);
},
willSendResponse(requestContext) {
const duration = Date.now() - requestContext.request.startTime;
console.log(`Request completed in ${duration}ms`);
},
};
},
},
],
});
2. Error Tracking
// monitoring/errorTracking.js
const Sentry = require("@sentry/node");
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
});
const server = new ApolloServer({
typeDefs,
resolvers,
formatError: (error) => {
// Log to Sentry
Sentry.captureException(error);
// Log to console
console.error("GraphQL Error:", {
message: error.message,
locations: error.locations,
path: error.path,
stack: error.stack,
});
// Return sanitized error in production
if (process.env.NODE_ENV === "production") {
if (
error.message.includes("Database") ||
error.message.includes("Internal")
) {
return new Error("Internal server error");
}
}
return error;
},
});
Deployment
1. Docker Setup
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S graphql -u 1001
# Change ownership
RUN chown -R graphql:nodejs /app
USER graphql
# Expose port
EXPOSE 4000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:4000/health || exit 1
# Start application
CMD ["npm", "start"]
2. Production Configuration
// config/production.js
module.exports = {
server: {
port: process.env.PORT || 4000,
cors: {
origin: process.env.ALLOWED_ORIGINS?.split(",") || false,
credentials: true,
},
},
database: {
url: process.env.DATABASE_URL,
ssl: { rejectUnauthorized: false },
pool: {
min: 2,
max: 10,
},
},
redis: {
url: process.env.REDIS_URL,
retryDelayOnFailover: 100,
maxRetriesPerRequest: 3,
},
auth: {
jwtSecret: process.env.JWT_SECRET,
jwtExpiration: "7d",
},
apollo: {
introspection: false,
playground: false,
debug: false,
},
};
Kesimpulan
GraphQL menawarkan pendekatan yang powerful dan flexible untuk membangun API modern. Dengan memahami konsep core, implementing best practices, dan menggunakan tools yang tepat, Anda dapat membangun GraphQL API yang:
- Efficient: Mengurangi over-fetching dan under-fetching
- Type-safe: Strong typing system mencegah runtime errors
- Scalable: Proper architecture dan caching strategies
- Secure: Comprehensive authentication, authorization, dan validation
- Maintainable: Clean code structure dan comprehensive testing
- Observable: Proper monitoring dan error tracking
Kunci sukses adalah memulai dengan schema design yang baik, implementing resolvers yang efficient, dan secara bertahap menambahkan advanced features sesuai kebutuhan aplikasi Anda.



