TypeScript menjamin validasi tipe hanya pada fase kompilasi. Saat aplikasi React Native dieksekusi di perangkat pengguna, data respons jaringan dari backend berstatus unknown. Ketika backend mengalami pembaruan yang tidak kompatibel—seperti field bernilai null yang tak terduga atau tipe data integer yang tiba-tiba dikirim sebagai string—aplikasi sering mengalami fatal crash dengan galat klasik: TypeError: Cannot read property 'map' of undefined.

Untuk mencegah terminasi mendadak pada JavaScript thread React Native, validasi batas (runtime boundary validation) wajib diterapkan tepat pada lapisan HTTP client. Menggunakan zod dengan metode safeParse memungkinkan aplikasi mendeteksi penyimpangan kontrak API, menyajikan data fallback yang aman ke UI, dan melaporkan log anomali ke platform observability.

Bahaya Schema Drift dan Ilusi Type Assertion

Pola umum yang kerap memicu celah stabilitas adalah penggunaan type casting eksplisit pada respons HTTP:

// Anti-pattern: Type assertion menipu compiler
const response = await fetch('https://api.internal/products');
const data = (await response.json()) as ProductListResponse;
setProducts(data.items); // Crash seketika jika 'items' dikirim null

Casting dengan as ProductListResponse sama sekali tidak melakukan pengecekan data saat runtime. Jika backend mengalami schema drift atau API gateway memulangkan respons galat dalam format HTML/JSON berbeda, JavaScript runtime Hermes atau JSC akan terus mengeksekusi data korup tersebut hingga UI memanggil fungsi array atau mengakses properti bersarang.

Penyelesaian: Runtime Boundary Menggunakan Zod safeParse

Zod menyediakan dua mekanisme validasi: parse() dan safeParse(). Metode parse() melempar ZodError saat validasi gagal, mewajibkan penanganan via blok try/catch yang jika terlewat akan merusak alur eksekusi komponen. Sebaliknya, safeParse() mengembalikan objek berstruktur discriminated union tanpa melempar fatal exception.

Alternatif lebih ringan: Type guard manual menggunakan operator typeof bawaan JavaScript cukup untuk payload primitif satu field. Gunakan Zod saat payload memiliki struktur bersarang, properti opsional, atau array.

Implementasi Typed API Client dengan Safe Fallback

Berikut adalah abstraksi HTTP client modular yang memvalidasi skema payload, memberikan fallback data saat terjadi malformasi, serta mengirim laporan ke error tracker tanpa menghentikan user flow.

import { z } from 'zod';

// 1. Definisikan skema kontrak data
export const ProductSchema = z.object({
  id: z.string(),
  title: z.string(),
  price: z.number().nonnegative(),
  tags: z.array(z.string()).default([]),
});

export type Product = z.infer<typeof ProductSchema>;

export const ProductListSchema = z.object({
  items: z.array(ProductSchema),
  total: z.number(),
});

export type ProductList = z.infer<typeof ProductListSchema>;

// 2. Abstraksi boundary parser
type ValidationResult<T> = {
  data: T;
  hasSchemaError: boolean;
};

export async function fetchWithContract<T>(
  url: string,
  schema: z.ZodType<T>,
  fallbackData: T,
  reportError: (errorDetails: object) => void
): Promise<ValidationResult<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) {
      throw new Error(`HTTP_${res.status}`);
    }

    const json = await res.json();
    const parseResult = schema.safeParse(json);

    if (parseResult.success) {
      return { data: parseResult.data, hasSchemaError: false };
    }

    // Schema drift terdeteksi: catat detail mismatch ke monitoring
    reportError({
      url,
      issues: parseResult.error.issues,
      receivedPayload: json,
    });

    return { data: fallbackData, hasSchemaError: true };
  } catch (error) {
    reportError({
      url,
      exception: error instanceof Error ? error.message : 'UNKNOWN_NETWORK_ERROR',
    });

    return { data: fallbackData, hasSchemaError: true };
  }
}

Dilema trade-off pada pola ini: UI menerima fallbackData terprediksi (misalnya { items: [], total: 0 }) sehingga layar tetap dapat dirender tanpa memicu crash white-screen. Developer tetap mendapatkan notifikasi anomali kontrak pada dashboard monitoring.

Pengujian Otomatis Penanganan Payload Rusak

Uji ketahanan parser boundary terhadap skema korup menggunakan modul pengujian bawaan Node.js tanpa dependensi framework test tambahan.

// run: node --test test-contract.mjs
import assert from 'node:assert/strict';
import { z } from 'zod';

const UserResponseSchema = z.object({
  userId: z.string(),
  profile: z.object({
    displayName: z.string(),
    age: z.number().optional(),
  }),
  permissions: z.array(z.string()).default([]),
});

function validatePayload(rawPayload, fallback) {
  const result = UserResponseSchema.safeParse(rawPayload);
  if (!result.success) {
    return { data: fallback, isValid: false, issues: result.error.issues };
  }
  return { data: result.data, isValid: true, issues: [] };
}

const fallbackUser = {
  userId: 'anonymous',
  profile: { displayName: 'Tamu' },
  permissions: [],
};

// Case 1: Payload valid
const validPayload = {
  userId: 'usr_101',
  profile: { displayName: 'John Doe', age: 30 },
  permissions: ['READ', 'WRITE'],
};
const validResult = validatePayload(validPayload, fallbackUser);
assert.equal(validResult.isValid, true);
assert.equal(validResult.data.userId, 'usr_101');

// Case 2: Schema drift (backend mengirim profile null dan permissions bernilai string)
const corruptedPayload = {
  userId: 'usr_102',
  profile: null,
  permissions: 'INVALID_TYPE',
};
const corruptedResult = validatePayload(corruptedPayload, fallbackUser);
assert.equal(corruptedResult.isValid, false);
assert.deepEqual(corruptedResult.data, fallbackUser);
assert.equal(corruptedResult.issues.length > 0, true);

console.log('Semua pengujian kontrak boundary berhasil dijalankan.');

Pertimbangan Kinerja pada React Native

Meskipun Zod memberikan keamanan runtime, validasi objek masif di JavaScript thread dapat memicu frame drop. Terapkan strategi berikut untuk menjaga performa optimal:

  • Hindari Over-Validation: Jangan validasi seluruh struktur API respons raksasa jika view screen hanya membutuhkan sebagian properti. Gunakan skema ramping yang hanya mengekstrak field yang benar-benar dikonsumsi komponen.
  • Bypass Payload Besar: Untuk respons daftar data di atas ribuan baris, validasi skema menyeluruh dapat membebani CPU mobile. Pertimbangkan menggunakan skema parsial atau validasi sampel item pertama untuk memastikan struktur dasar array.
  • Normalisasi Default: Manfaatkan utility .default() dan .catch() pada field individual Zod agar fallback hanya diaplikasikan pada atribut yang rusak tanpa membatalkan seluruh record.