Episode ini membahas penyimpanan data di Remix: integrasi ORM seperti Prisma, setup connection pooling dengan konfigurasi berbasis environment, querying di loader dan action, serta transaction handling untuk operasi yang aman.

Hingga episode 9, semua data kalian hidup dalam array dan objek yang hilang saat server restart. Aplikasi serius butuh persistence: data tersimpan di database dan bertahan di luar proses. Di sinilah Remix menunjukkan kekuatannya, karena query database ditulis langsung di loader dan action — di server.
Pendekatan ini menghilangkan lapisan API tambahan. Kalian tidak perlu membangun REST endpoint khusus untuk data internal; loader cukup memanggil fungsi repository, dan action menyimpan perubahan. Hasilnya kode lebih sedikit, jenis lebih aman, dan alur lebih mudah diikuti.
Episode 10 akan memakai Prisma sebagai ORM contoh, lalu membahas koneksi dan pooling, querying di loader dan action, serta transaction handling.
Prisma adalah ORM TypeScript paling populer di ekosistem Remix. Install lalu inisialisasi dengan driver PostgreSQL atau SQLite untuk belajar:
npm install prisma @prisma/client
npx prisma init --datasource-provider postgresqlPerintah prisma init membuat folder prisma dan file .env untuk DATABASE_URL. Skema database ditulis dalam Prisma Schema Language, bukan SQL manual.
Model didefinisikan di prisma/schema.prisma, lalu disinkronkan ke database:
model Post {
id String @id @default(cuid())
judul String
konten String
createdAt DateTime @default(now())
}Setelah model ditulis, jalankan prisma migrate dev untuk membuat tabel. Prisma menghasilkan tipe TypeScript yang bisa dipakai langsung di loader dan action.
Saat development, hot reload membuat banyak instance PrismaClient jika tidak ditangani. Solusi standar adalah singleton global:
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis;
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}Pola ini memastikan satu instance PrismaClient dipakai ulang selama process hidup. Ini mencegah pembocoran koneksi di development dan mempercepat startup.
Untuk serverless yang memulai process baru terus-menerus, setiap process membuat koneksi baru — berisiko menghabiskan batas koneksi database. Gunakan connection pooler seperti PgBouncer atau Supabase Pooler. Atur pool size sesuai env dengan env var seperti DATABASE_POOL_SIZE agar development dan production memakai nilai berbeda.
Loader adalah tempat query baca. Data dikembalikan langsung ke komponen:
import { prisma } from "~/lib/prisma.server";
import { useLoaderData } from "@remix-run/react";
export async function loader() {
const posts = await prisma.post.findMany({
orderBy: { createdAt: "desc" },
take: 10,
});
return { posts };
}
export default function Daftar() {
const { posts } = useLoaderData();
return (
<ul>
{posts.map((p) => <li key={p.id}>{p.judul}</li>)}
</ul>
);
}Loader memanggil prisma.post.findMany langsung di server. Tidak ada endpoint API perantara — query dan rendering hidup di satu file route.
Action menulis perubahan. Dengan idempotency dan validasi sebelum menulis:
export async function action({ request }) {
const formData = await request.formData();
const judul = String(formData.get("judul"));
const post = await prisma.post.create({
data: { judul },
});
return redirect(`/posts/${post.id}`);
}Prisma.create mengembalikan data yang baru dibuat, lalu redirect membawa pengguna ke detail. Karena loader route di-refresh otomatis setelah action, daftar posting selalu sinkron tanpa kode tambahan.
Saat satu aksi harus mengubah beberapa tabel secara bersamaan, gunakan transaction agar semua berhasil atau semuanya gagal. Prisma.$transaction menjalankan beberapa operasi secara atomik.
export async function action({ request }) {
const formData = await request.formData();
const postId = String(formData.get("postId"));
await prisma.$transaction([
prisma.viewLog.create({ data: { postId } }),
prisma.post.update({ where: { id: postId }, data: { dilihat: { increment: 1 } } }),
]);
return redirect(`/posts/${postId}`);
}Array operasi di $transaction dieksekusi bersama; jika satu gagal, semuanya dibatalkan. Ini penting untuk data yang harus konsisten, seperti counter dan log.
Loader mengembalikan semua field model secara default. Jangan kirim data sensitif ke client. Pilih field dengan select di Prisma, atau buat fungsi serializer di modul tersendiri. Praktik ini akan dibahas lagi di episode 13 tentang keamanan dan episode 18 tentang arsitektur.
Episode 10 menghubungkan aplikasi kalian dengan database: Prisma untuk ORM dan schema, singleton PrismaClient dengan connection pooling, querying langsung di loader dan action, serta transaction untuk konsistensi data. Persistence kini bukan lagi kata asing.
Inti yang harus dibawa pulang:
Di episode 11 selanjutnya kita akan membahas authentication dan session — pola autentikasi di Remix, session handling dengan cookies, proteksi route dan user context, serta OAuth dan social login dengan sesi yang aman. Database sudah menyimpan data; sekarang saatnya memastikan hanya orang yang berhak yang mengaksesnya.