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
Dateyang dikembalikan olehhandlertetap diterima sebagai objekDatevalid pada browser tanpa konversi manual. - Nuxt Nitro: Secara bawaan mengeksekusi
JSON.stringifypada response payload. Properti objek bernilaiDateotomatis berubah menjadistringISO 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.tsyang 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.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!