89 Penulisan Resolver yang Bersih dan Reusable: Panduan Engineer
Tidak sedikit dari kita, para engineer backend, pernah bersentuhan dengan GraphQL. Salah satu elemen kunci dalam mengembangkan GraphQL Server adalah resolver. Namun, sementara GraphQL memberikan kebebasan dan fleksibilitas, seringkali resolver yang kita tulis jadi cepat membengkak, berulang-ulang, susah diatur, dan sulit di-reuse. Artikel ini akan membahas 89 penulisan resolver yang bersih dan reusable—(tentu, angka 89 adalah hiperbolis, namun maknanya: there are takeaways for everyone!) Disertai contoh kode, simulasi, hingga diagram mermaid untuk memperjelas konsepnya.
Apa Itu Resolver?
Sebelum menulis lebih jauh, mari sedikit merefresh tentang resolver. Resolver adalah fungsi/handler yang menjalankan proses pengambilan data pada masing-masing field di schema GraphQL.
const resolvers = {
Query: {
user: (_, { id }) => {
return users.find((user) => user.id === id);
},
},
};
Sederhana bukan? Tapi ketika schema tumbuh, permintaan bisnis bertambah, dan error handling jadi semakin komplek, resolver kita bisa berubah jadi seperti ini (yang mungkin terasa familiar):
const resolvers = {
Query: {
user: async (_, { id }, context) => {
try {
if (!context.user) {
throw new Error('Unauthorized');
}
const user = await db.getUserById(id);
if (!user) {
throw new Error('User not found');
}
// Logging, transform, dsb.
context.logger.log('fetching user:', id);
return user;
} catch (err) {
context.logger.error(err);
throw err;
}
},
},
};
Resolver seperti ini sering kali mudah tumbuh membengkak dan sulit di-reuse. Lalu, bagaimana menulis resolver yang bersih dan reusable?
Prinsip-Prinsip Penulisan Resolver Bersih
Mari bedah beberapa prinsip fundamentalnya (tidak terbatas 89 ya!):
Prinsip | Penjelasan Singkat |
---|---|
Single Responsibility | Setiap resolver melakukan SATU hal spesifik. |
Modular | Ekstrak fungsi-fungsi umum ke util/helper terpisah. |
Jangan Repetisi | Hindari copy-paste bolak-balik, pakai abstraksi. |
Gunakan Middleware | Injeksi logika cross-cutting (auth, logging) via middleware. |
Typed | Gunakan tipe data (TypeScript) untuk validasi dan autocomplete. |
Keringkan Error Handling | Centralized error handler jika memungkinkan. |
Gunakan Loader/Cache | Pakai DataLoader/similar untuk batch dan cache permintaan DB. |
Unit Testable | Pastikan setiap resolver gampang dipisah test-nya. |
Contoh Kode: Penyusunan Resolver Modular & Reusable
Mari kita urai dari contoh resolver jelek di atas jadi beberapa bagian modular yang reusable.
1. Abstraksi Context (Authorization sebagai Middleware)
Alih-alih semua resolver ngecek context.user
satu-satu, gunakan higher-order resolver (middleware):
// middleware/requireAuth.js
function requireAuth(resolver) {
return (parent, args, context, info) => {
if (!context.user) {
throw new Error('Unauthorized');
}
return resolver(parent, args, context, info);
};
}
2. Modularisasi Fungsi CRUD
Pisahkan fungsi DB dan transformasi data ke luar resolver:
// db/user.js
async function getUserById(id) {
// misal: akses ke Postgres/Mongo
return await db.users.findOne({ id });
}
// helpers/logger.js
function logRequest(logger, message) {
logger.log(`[Request]: ${message}`);
}
3. Gunakan DataLoader
Untuk menghindari N+1 problem:
// loaders/userLoader.js
const DataLoader = require('dataloader');
const { getUsersByIds } = require('../db/user');
const userLoader = new DataLoader(async (ids) => {
const users = await getUsersByIds(ids);
return ids.map((id) => users.find((user) => user.id === id));
});
module.exports = userLoader;
4. Centralized Error Handling (Optional)
Daripada tiap resolver try-catch, bisa via middleware juga:
function withErrorHandler(resolver) {
return async (parent, args, context, info) => {
try {
return await resolver(parent, args, context, info);
} catch (err) {
context.logger.error(err);
throw err;
}
};
}
5. Merangkai Resolver dengan Middleware
Modularisasi memungkinkan kita memakai komposisi:
const { getUserById } = require('./db/user');
const requireAuth = require('./middleware/requireAuth');
const withErrorHandler = require('./middleware/withErrorHandler');
const userResolver = withErrorHandler(
requireAuth(async (parent, { id }, context) => {
context.logger.log('Fetching user:', id);
return await getUserById(id); // fungsi terpisah, testable
}),
);
// Di resolver utama:
const resolvers = {
Query: {
user: userResolver,
},
};
Visualisasi Flow Resolver (Mermaid)
Diagram berikut mengilustrasikan alur resolver modular di atas:
graph TD A[Client Query] --> B[Resolver Utama] B --> C[withErrorHandler] C --> D[requireAuth] D --> E[Mengambil Data dari DB] E --> F[Return Data] C --error--> G[Logger Error]
Simulasi Query
Agar lebih nyata, simulasikan kasus berikut:
- Request dengan user yang belum login
- Request dengan id user yang tidak ada
- Request sukses
Kode test (pseudo):
const context = { user: null, logger: console }; // tidak login
try {
await resolvers.Query.user(null, { id: 1 }, context);
} catch (err) {
console.log('Result:', err.message); // --> Unauthorized
}
context.user = { id: 12, name: 'Alice' };
try {
await resolvers.Query.user(null, { id: '99999' }, context);
} catch (err) {
console.log('Result:', err.message); // --> User not found
}
context.user = { id: 1, name: 'Bob' };
try {
const user = await resolvers.Query.user(null, { id: 1 }, context);
console.log('Result:', user); // Data user sukses
} catch (err) {}
Tabel: Format Resolver Reusable
Bagian | Modularisasi |
---|---|
Otentikasi | Middleware requireAuth |
Logging | Helper/logger terpisah |
Query DB | Helper getUserById dsb |
Error Handling | Middleware/error-handler |
Loader/Cache | DataLoader, dsb |
Kasus-kasus field resolver | Helper reusable pada field-level |
Pattern-Pola Resolver yang Reusable
- Resolver Maker: Buat factory fungsi pembuat resolver jika banyak schema mirip.
function makeGetByIdResolver(getterFn) { return async (_, { id }) => await getterFn(id); } const resolvers = { Query: { user: makeGetByIdResolver(getUserById), product: makeGetByIdResolver(getProductById), }, };
- Pakai Library Utility: Gunakan package seperti graphql-middleware untuk middleware pipeline;
- Generic Error Handler: Implementasikan error handler tersentral.
Kapan Harus Modular, Kapan Tidak?
Over-engineering adalah musuh engineer. Saat schema kecil dan tidak berubah-ubah cukup lama, argumen membuat “clean” bisa diabaikan, lebih baik maintainable. Namun, jika tim dan kodebase membesar, clean & reusable resolver bukan hanya “bonus”, tetapi necessity!
Kesimpulan
Menulis 89 resolver yang clean dan reusable? Kuncinya: modularisasi, DRY, dan jangan segan gunakan middleware! Selain bikin codebase mudah dirawat dan dikembangkan tim, kualitas produk backend-mu dijamin naik tingkat. Dengan tools seperti DataLoader, middleware pattern, serta helper/factory sederhana, healthy resolver stack adalah keniscayaan, bukan impian. Bangun satu kali, nikmati selamanya.
Referensi
- https://graphql.org/learn/execution/
- https://www.apollographql.com/docs/apollo-server/data/resolvers/
- https://github.com/prisma-labs/graphql-middleware
Semoga artikel ini jadi referensi dan peta jalan buat timmu yang ingin codebase GraphQL semakin professional. Jika ada tips pattern lain, share di komentar! 🚀
Artikel Terhangat
90 Strategi Pengujian dan CI/CD graphql-go
09 Sep 2025
88 Struktur Project graphql-go yang Scalable
09 Sep 2025
87 Tips Debugging Resolver dan Query GraphQL
09 Sep 2025

90 Strategi Pengujian dan CI/CD graphql-go

88 Struktur Project graphql-go yang Scalable
