tutorial

89 Penulisan Resolver yang Bersih dan Reusable

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!):

PrinsipPenjelasan Singkat
Single ResponsibilitySetiap resolver melakukan SATU hal spesifik.
ModularEkstrak fungsi-fungsi umum ke util/helper terpisah.
Jangan RepetisiHindari copy-paste bolak-balik, pakai abstraksi.
Gunakan MiddlewareInjeksi logika cross-cutting (auth, logging) via middleware.
TypedGunakan tipe data (TypeScript) untuk validasi dan autocomplete.
Keringkan Error HandlingCentralized error handler jika memungkinkan.
Gunakan Loader/CachePakai DataLoader/similar untuk batch dan cache permintaan DB.
Unit TestablePastikan 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

BagianModularisasi
OtentikasiMiddleware requireAuth
LoggingHelper/logger terpisah
Query DBHelper getUserById dsb
Error HandlingMiddleware/error-handler
Loader/CacheDataLoader, dsb
Kasus-kasus field resolverHelper reusable pada field-level

Pattern-Pola Resolver yang Reusable

  1. 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),
      },
    };
    
  2. Pakai Library Utility: Gunakan package seperti graphql-middleware untuk middleware pipeline;
  3. 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

Semoga artikel ini jadi referensi dan peta jalan buat timmu yang ingin codebase GraphQL semakin professional. Jika ada tips pattern lain, share di komentar! 🚀

comments powered by Disqus