Membangun Kontrak RPC End-to-End Type-Safe

Komunikasi client-server pada framework full-stack modern bergeser dari deklarasi REST API terpisah ke arah Remote Procedure Call (RPC) dengan inferensi tipe statis. TanStack Start dan Nuxt 3 (via Nitro engine) menyelesaikan masalah kontrak tipe ini dengan filosofi arsitektur yang berbeda.

TanStack Start mengandalkan Server Functions (createServerFn) berbasis compiler swap untuk mengubah deklarasi fungsi menjadi RPC otomatis. Di sisi lain, Nuxt Nitro menggunakan sistem berkas virtual berdasar direktori server/api atau server/routes dengan helper defineEventHandler dan fungsi fetch bawaan ($fetch) yang menginferensi tipe respons dari return value backend.

Validasi Skema Runtime vs Inferensi Tipe Otomatis

TypeScript murni hanya memvalidasi tipe saat kompilasi (compile-time). Tanpa validasi runtime, payload request rentan terhadap injeksi tipe data invalid atau struktur payload yang bermutasi. Kedua framework menangani sinkronisasi tipe runtime dan statis secara berbeda.

1. TanStack Start: Deklarasi Skema Terintegrasi

TanStack Start Server Functions menyediakan rantai method terintegrasi menggunakan validator schema seperti Zod. Tipe input didefinisikan secara eksplisit dan dipaksa valid pada level eksekusi fungsi sebelum logika inti backend dijalankan.

import { createServerFn } from '@tanstack/start';
import { z } from 'zod';

const UpdateProfileSchema = z.object({
  userId: z.string().uuid(),
  displayName: z.string().min(3),
});

export const updateProfileFn = createServerFn({ method: 'POST' })
  .validator((data: unknown) => UpdateProfileSchema.parse(data))
  .handler(async ({ data }) => {
    // data terinferensi secara pasti sebagai { userId: string, displayName: string }
    return {
      success: true,
      updatedAt: new Date(),
    };
  });

2. Nuxt Nitro: Server Event Handler Manual

Nuxt mengekstraksi tipe kembalian fungsi secara otomatis ke dalam namespace global Nitro. Namun, validasi input body membutuhkan panggilan manual ke readValidatedBody untuk mengikat skema runtime ke inferensi tipe handler.

// server/api/profile.post.ts
import { defineEventHandler, readValidatedBody } from 'h3';
import { z } from 'zod';

const UpdateProfileSchema = z.object({
  userId: z.string().uuid(),
  displayName: z.string().min(3),
});

export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, (data) => UpdateProfileSchema.parse(data));

  return {
    success: true,
    updatedAt: new Date().toISOString(),
  };
});

Penggunaan pada Sisi Client (Mutation Flow)

Di client, TanStack Start memanggil updateProfileFn({ data: payload }) secara langsung seolah-olah mengimpor modul lokal. Bundler (Vite) mengganti implementasi fungsi server dengan stub pemanggil fetch RPC. Pada Nuxt, pemanggilan dilakukan menggunakan path string via $fetch('/api/profile') atau composable useFetch.

// TanStack Start (Komponen Client)
import { useMutation } from '@tanstack/react-query';
import { updateProfileFn } from '../server/functions';

const mutation = useMutation({
  mutationFn: (payload: { userId: string; displayName: string }) =>
    updateProfileFn({ data: payload }),
});
// Nuxt 3 (Komponen Vue)
<script setup lang="ts">
// Tipe respons dan error terinferensi secara otomatis dari path literal
const updateProfile = async (payload: { userId: string; displayName: string }) => {
  const result = await $fetch('/api/profile', {
    method: 'POST',
    body: payload,
  });
};
</script>

Konsep Auth Context dan Akses Cookie SSR

RPC internal server framework harus mampu membaca konteks sesi, header, dan cookie HTTP secara aman, baik saat dieksekusi via SSR render pipeline maupun RPC invocation standar melalui HTTP round-trip.

Middleware Context pada TanStack Start

TanStack Start mengabstraksi konteks melalui pipeline middleware bawaan. Akses header dan cookie didapatkan dari runtime adapter layer (seperti Vinxi/WebRequest).

import { createMiddleware } from '@tanstack/start';
import { getCookie } from 'vinxi/http';

const authMiddleware = createMiddleware().server(async ({ next }) => {
  const token = getCookie('session_token');
  if (!token) {
    throw new Error('Unauthorized');
  }
  return next({ context: { userId: 'usr_123' } });
});

export const getSensitiveData = createServerFn({ method: 'GET' })
  .middleware([authMiddleware])
  .handler(async ({ context }) => {
    return { owner: context.userId };
  });

Event Context pada Nuxt Nitro

Nuxt memanfaatkan hook Nitro dan helper h3 untuk inject konteks ke dalam objek event.context melalui middleware server.

// server/middleware/auth.ts
import { defineEventHandler, getCookie, createError } from 'h3';

export default defineEventHandler((event) => {
  const token = getCookie(event, 'session_token');
  if (!token && event.path.startsWith('/api/protected')) {
    throw createError({ statusCode: 401, statusMessage: 'Unauthorized' });
  }
  event.context.auth = { userId: 'usr_123' };
});

Serialisasi Non-JSON dan Mitigasi State

Salah satu batas kerapuhan RPC adalah kehilangan representasi tipe data non-JSON primitif (misalnya objek Date, BigInt, Map, atau Set) saat payload transit melalui JSON parser standar.

  • TanStack Start: Mendukung parser serialisasi canggih (seperti SuperJSON) yang dapat dikonfigurasikan secara transparan. Nilai Date yang dikembalikan oleh handler tetap diterima sebagai objek Date valid pada browser tanpa konversi manual.
  • Nuxt Nitro: Secara bawaan mengeksekusi JSON.stringify pada response payload. Properti objek bernilai Date otomatis berubah menjadi string ISO saat diterima oleh $fetch. Developer wajib melakukan parsing tanggal secara manual di client, atau menambahkan transformer runtime tambahan secara mandiri.

Propagasi Error Boundary

Penanganan error yang deterministik memerlukan pemisahan antara operational error (kesalahan validasi form) dan fatal error (kegagalan database atau unhandled exception).

Nuxt menyediakan fungsi utilitas createError() yang menghentikan eksekusi handler dan langsung mereturn payload standar H3Error ke client lengkap dengan statusCode. Di client, composable useFetch menangkapnya dalam properti error.value.

TanStack Start melempar exception standar yang dapat ditangkap oleh React Query onError boundary atau TanStack Router error component. Jika terjadi unhandled internal server error, informasi stack trace dipangkas pada mode produksi demi alasan keamanan, dan client menerima respons generik status 500.

Analisis Bundle Client dan Dependensi

Dampak ukuran bundle client bervariasi bergantung pada mekanisme RPC yang diterapkan:

  • Overhead TanStack Start: Mengimpor skema Zod secara langsung di dalam deklarasi Server Function berisiko membawa dependensi Zod ke bundle client jika pemecahan modul (code elimination) bundler gagal mengisolasi validator tree. Praktik terbaik: pisahkan skema Zod ke berkas tipe murni atau gunakan dynamic import untuk payload handler jika bundler belum memisahkan server code secara sempurna.
  • Overhead Nuxt Nitro: Kode server pada direktori server/ tidak pernah disentuh oleh bundler client. Client hanya mengimpor string path rute HTTP (misal: /api/profile). Nitro menghasilkan file .nuxt/nitro.d.ts yang memetakan path string tersebut ke fungsi handler. Bundle client tetap bebas dari runtime validator backend.

Panduan Memilih Arsitektur

Gunakan TanStack Start Server Functions jika:

  • Aplikasi membutuhkan full-stack type safety yang terpusat dalam satu file (kolokasi logic frontend dan backend RPC).
  • Aplikasi sering mengirimkan tipe data non-JSON murni (Date, Map, Set) dan memerlukan serialisasi dua arah yang presisi.
  • Aplikasi dibangun di atas ekosistem React 19 dan TanStack ecosystem (Router + Query).

Gunakan Nuxt Server Routes jika:

  • Aplikasi memerlukan API server yang independen dan dapat diakses oleh consumer eksternal (mobile app, third-party webhook) tanpa perlakuan khusus format RPC.
  • Memerlukan ukuran bundle client seminimal mungkin tanpa risiko bocornya pustaka validasi server ke client bundle.
  • Menginginkan struktur direktori yang strictly decoupled antara UI logic dan backend endpoints.