GRAPHQL API-DESIGN ERROR-HANDLING BACKEND WEB-DEVELOPMENT API SYSTEM-DESIGN BEST-PRACTICES DEVELOPER-EXPERIENCE

Pola Penanganan Error di GraphQL: Membangun API yang Robust dan User-Friendly

⏱️ 12 menit baca
👨‍💻

Pola Penanganan Error di GraphQL: Membangun API yang Robust dan User-Friendly

1. Pendahuluan

Sebagai developer, kita tahu bahwa error adalah bagian tak terpisahkan dari setiap aplikasi. Namun, cara kita menangani dan mengkomunikasikan error ini kepada klien (frontend, aplikasi mobile, atau layanan lain) sangat krusial untuk pengalaman developer (DX) dan pengalaman pengguna (UX). Di dunia GraphQL, penanganan error memiliki karakteristik unik yang berbeda dari REST API tradisional.

Secara default, GraphQL memiliki mekanisme error handling bawaan, tapi seringkali itu tidak cukup informatif atau fleksibel untuk kebutuhan aplikasi modern yang kompleks. Bayangkan pengguna mencoba login, tapi gagal. Pesan error default yang generik seperti “Internal Server Error” atau “Validation Error” tidak membantu mereka memahami apa yang salah, apalagi membantu developer frontend mendebug masalahnya.

Di artikel ini, kita akan menyelami berbagai pola dan strategi untuk menangani error di GraphQL. Kita akan mulai dari memahami mekanisme bawaan, kemudian menjelajahi bagaimana kita bisa memperkaya informasi error, hingga mengimplementasikan pola “Error as Data” yang type-safe, dan diakhiri dengan praktik terbaik untuk membangun API GraphQL yang robust dan user-friendly.

🎯 Mengapa Penanganan Error di GraphQL Penting?

Mari kita mulai petualangan kita dalam menguasai penanganan error di GraphQL!

2. Memahami Struktur Error GraphQL Standar

Ketika terjadi error di GraphQL, server akan mengembalikan respons HTTP 200 OK (ini seringkali mengejutkan developer yang terbiasa dengan status HTTP seperti 4xx atau 5xx untuk error di REST) bersama dengan objek JSON yang berisi array errors.

Berikut adalah contoh respons error standar GraphQL:

{
  "errors": [
    {
      "message": "Field 'email' is required.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createUser"
      ]
    }
  ],
  "data": null
}

Setiap objek di array errors biasanya memiliki properti berikut:

📌 Keterbatasan Error Standar: Meskipun memberikan informasi dasar, error standar ini seringkali kurang spesifik. Misalnya, dari message: "Field 'email' is required.", kita tahu email dibutuhkan, tapi kita tidak tahu kode error spesifik, atau informasi tambahan yang relevan untuk UI (misalnya, apakah ini error validasi, error otorisasi, atau error server internal?).

Ini membuat penanganan error di sisi klien menjadi lebih sulit karena harus melakukan parsing string message atau bergantung pada path dan locations yang tidak selalu konsisten untuk berbagai jenis error.

3. Memperkaya Error dengan extensions

Salah satu cara paling umum dan direkomendasikan untuk menambahkan konteks ke error GraphQL adalah dengan menggunakan properti extensions. Properti ini adalah objek kustom yang bisa Anda isi dengan data tambahan apa pun yang relevan.

💡 Contoh Penggunaan extensions:

Misalnya, kita ingin memberikan kode error spesifik, status HTTP yang lebih bermakna (meskipun respons HTTP utama tetap 200), dan detail validasi.

# Definisi error di server (contoh menggunakan Apollo Server)
type Query {
  getUser(id: ID!): User
}

type Mutation {
  createUser(input: UserInput!): UserPayload
}

input UserInput {
  name: String!
  email: String!
  password: String!
}

type User {
  id: ID!
  name: String!
  email: String!
}

type UserPayload {
  user: User
  errors: [UserError]
}

interface UserError {
  message: String!
  code: String!
}

type ValidationError implements UserError {
  message: String!
  code: String!
  field: String!
  value: String
}

type AuthenticationError implements UserError {
  message: String!
  code: String!
}

Dan implementasi di resolver (pseudo-code):

// Contoh di Node.js dengan Apollo Server
const { GraphQLError } = require('graphql');

const resolvers = {
  Mutation: {
    createUser: (parent, { input }) => {
      // Validasi input
      if (!input.email || !input.email.includes('@')) {
        throw new GraphQLError('Email tidak valid.', {
          extensions: {
            code: 'BAD_USER_INPUT',
            httpStatus: 400,
            field: 'email',
            details: 'Format email tidak benar.'
          }
        });
      }

      // Logika pembuatan user
      // ...
      // Jika user sudah ada
      if (userExists(input.email)) {
        throw new GraphQLError('Email sudah terdaftar.', {
          extensions: {
            code: 'DUPLICATE_RESOURCE',
            httpStatus: 409,
            field: 'email'
          }
        });
      }

      // Jika ada error internal server
      try {
        // ... operasi database ...
      } catch (error) {
        throw new GraphQLError('Terjadi kesalahan internal server.', {
          extensions: {
            code: 'INTERNAL_SERVER_ERROR',
            httpStatus: 500,
            traceId: 'abc-123', // Untuk debugging
            originalError: error.message // Hati-hati jangan ekspos terlalu banyak
          }
        });
      }

      return { /* user data */ };
    },
  },
};

Dengan pendekatan ini, respons error akan terlihat seperti:

{
  "errors": [
    {
      "message": "Email tidak valid.",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ],
      "path": [
        "createUser"
      ],
      "extensions": {
        "code": "BAD_USER_INPUT",
        "httpStatus": 400,
        "field": "email",
        "details": "Format email tidak benar."
      }
    }
  ],
  "data": null
}

Keuntungan extensions:

Kekurangan extensions:

4. Pola “Error as Data” (Union/Interface Types)

Pola ini melihat error bukan sebagai “pengecualian” yang selalu memicu array errors di respons, melainkan sebagai “hasil alternatif” dari suatu operasi. Dengan kata lain, suatu operasi bisa mengembalikan data yang sukses, atau data yang merepresentasikan error, keduanya didefinisikan secara eksplisit dalam skema GraphQL.

Analogi sederhananya: Ketika Anda meminta minuman di kafe, Anda bisa mendapatkan kopi yang Anda pesan (sukses), atau Anda bisa diberi tahu bahwa kopi habis (error). Keduanya adalah “hasil” dari permintaan Anda.

💡 Implementasi “Error as Data”: Kita akan menggunakan Union atau Interface types di skema GraphQL.

Pertama, definisikan tipe untuk hasil sukses dan tipe untuk error:

# Skema GraphQL
type Query {
  # ...
}

type Mutation {
  login(email: String!, password: String!): LoginResult!
}

type User {
  id: ID!
  email: String!
  name: String
}

# Definisi Union Type untuk hasil login
union LoginResult = AuthSuccess | AuthError

type AuthSuccess {
  token: String!
  user: User!
}

interface AuthError {
  code: String!
  message: String!
}

type InvalidCredentialsError implements AuthError {
  code: String!
  message: String!
}

type UserNotFoundError implements AuthError {
  code: String!
  message: String!
}

# ... tipe error lainnya

Kemudian, di resolver Anda, alih-alih melempar error, Anda akan mengembalikan objek error yang sesuai dengan skema:

// Contoh resolver untuk login
const resolvers = {
  Mutation: {
    login: async (parent, { email, password }) => {
      const user = await findUserByEmail(email);

      if (!user) {
        return {
          __typename: 'UserNotFoundError',
          code: 'USER_NOT_FOUND',
          message: 'Pengguna dengan email tersebut tidak ditemukan.'
        };
      }

      if (!await verifyPassword(user.id, password)) {
        return {
          __typename: 'InvalidCredentialsError',
          code: 'INVALID_CREDENTIALS',
          message: 'Kombinasi email dan kata sandi salah.'
        };
      }

      const token = generateAuthToken(user);
      return {
        __typename: 'AuthSuccess',
        token,
        user
      };
    }
  },
  LoginResult: {
    __resolveType(obj, context, info) {
      if (obj.token && obj.user) {
        return 'AuthSuccess';
      }
      if (obj.code && obj.message) {
        // Ini adalah cara sederhana, mungkin perlu properti yang lebih spesifik
        // untuk membedakan antara InvalidCredentialsError dan UserNotFoundError
        if (obj.code === 'USER_NOT_FOUND') {
          return 'UserNotFoundError';
        }
        if (obj.code === 'INVALID_CREDENTIALS') {
          return 'InvalidCredentialsError';
        }
      }
      return null;
    },
  },
};

Klien kemudian akan query seperti ini:

mutation UserLogin($email: String!, $password: String!) {
  login(email: $email, password: $password) {
    ... on AuthSuccess {
      token
      user {
        id
        name
        email
      }
    }
    ... on InvalidCredentialsError {
      code
      message
    }
    ... on UserNotFoundError {
      code
      message
    }
    # ... handle other error types
  }
}

Respons sukses:

{
  "data": {
    "login": {
      "token": "...",
      "user": {
        "id": "123",
        "name": "John Doe",
        "email": "john@example.com"
      }
    }
  }
}

Respons error (sebagai data):

{
  "data": {
    "login": {
      "code": "INVALID_CREDENTIALS",
      "message": "Kombinasi email dan kata sandi salah."
    }
  }
}

Keuntungan “Error as Data”:

Kekurangan “Error as Data”:

5. Strategi Penanganan Error Global (Middleware/Interceptor)

Selain penanganan error di level resolver, penting juga untuk memiliki mekanisme penanganan error global. Ini berguna untuk: