Dev Notes
Workflow AI Saya untuk Bikin Boilerplate Fullstack dalam 15 Menit
Panduan praktis merangkai prompt chaining, template Prisma schema, dan AI tools untuk scaffolding micro-SaaS tanpa setup manual yang berulang.

Berapa kali dalam setahun Anda membuat project baru dari nol? Kalau Anda tipe engineer yang sering bereksperimen dengan ide micro-SaaS atau internal tools, ritualnya selalu sama dan membosankan: setup Next.js, konfigurasi Tailwind, pasang ORM, bikin skema autentikasi, set up session handler, pasang Zod, bikin form validation, pasang komponen UI, lalu setup error boundary.
Ritual manual ini biasanya menghabiskan waktu 2 sampai 3 hari kerja sebelum Anda benar-benar menulis satu baris pun business logic yang unik untuk produk Anda.
Banyak engineer mencoba menyelesaikan masalah ini dengan dua cara ekstrem:
- Membeli atau cloning *fat starter kit* serba ada: Starter kit 10.000 baris kode yang membengkak dengan 40 library berbeda yang setengahnya tidak Anda butuhkan dan susah di-maintenance.
- Meminta LLM membuatkan satu aplikasi utuh dari satu prompt: "Tolong buatkan SaaS invoice lengkap dengan Next.js dan database." Hasilnya? Kode berantakan, relasi database cacat, file raksasa 800 baris tanpa modularitas, dan dependensi usang yang langsung error saat di-build.
Di artikel ini, saya akan membedah workflow personal yang saya pakai sehari-hari di Vour Studio. Dengan menggabungkan deterministic scaffolding (fondasi minimalis yang pasti) dan prompt chaining berbasis konteks schema, kita bisa melahirkan boilerplate fullstack production-ready yang type-safe dari database sampai UI dalam waktu kurang dari 15 menit.
Fondasi: Anatomi Stack yang Dipilih
Sebelum menyentuh AI, Anda harus menentukan opinionated baseline stack. AI bekerja optimal bukan saat diberi kebebasan tanpa batas, melainkan saat diberi pagar pembatas (guardrails) yang sangat jelas.
Stack yang saya gunakan untuk workflow ini:
- Framework: Next.js (App Router, Server Actions).
- Database & ORM: PostgreSQL + Prisma.
- Validasi: Zod +
next-safe-action. - UI Library: Tailwind CSS + shadcn/ui.
- Auth: Auth.js v5 atau Supabase Auth (berbasis session cookies).
Semua komponen di atas memiliki sifat yang sama: TypeScript-first dan memiliki skema deklaratif. Hal ini memudahkan AI memahami batas-batas tipe data tanpa perlu menebak-nebak runtime state.
Mari kita pecah 15 menit tersebut menjadi empat tahap eksekusi terstruktur.
Tahap 1: Setup Workspace & System Prompt Context Engine (Menit 0–3)
Langkah pertama bukan membuka ChatGPT di browser, melainkan menyiapkan workspace dengan file .cursorrules atau System Instructions di IDE (Cursor / VS Code Copilot).
Tanpa instruksi arsitektural di level workspace, AI akan mencampuradukkan pola lama (seperti Next.js Pages Router atau fetch() manual di useEffect) dengan pola modern.
Bikin project dasar menggunakan package manager favorit Anda:
pnpm create next-app@latest my-saas --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
cd my-saas
pnpm add prisma @prisma/client zod next-safe-action lucide-react clsx tailwind-merge
pnpm add -D prisma
pnpm dlx shadcn@latest initSetelah scaffolding dasar terpasang, buat file .cursorrules (atau .windsurfrules / .github/copilot-instructions.md) di root directory project Anda.
# Architecture & Coding Standards
Anda adalah Senior Fullstack Engineer yang membangun micro-SaaS modern. Patuhi aturan berikut tanpa pengecualian:
## 1. Stack & Paradigma
- Next.js App Router (React 19 / Server Components secara default).
- Gunakan 'use client' HANYA jika komponen membutuhkan event listener, browser API, atau client state.
- Database access HANYA dilakukan melalui Prisma Client di Server Components atau Server Actions.
## 2. Mutasi Data & Keamanan
- DILARANG membuat API Routes (/api/*) untuk mutasi data internal. Gunakan Server Actions dengan wrapper `next-safe-action`.
- Setiap Server Action WAJIB memiliki skema input Zod yang ketat.
- Selalu periksa autentikasi session pengguna sebelum mengeksekusi mutasi.
## 3. UI & Styling
- Gunakan komponen shadcn/ui dan Tailwind CSS.
- Hindari inline style dan jangan membuat file `.module.css`.
- Tangani loading state menggunakan React `useTransition` atau Suspense boundaries.Dengan file aturan ini aktif, AI tidak akan lagi menyarankan axios, tidak akan membuat route handler lama, dan langsung menulis kode sesuai konvensi App Router.
Tahap 2: Skema Database sebagai Single Source of Truth (Menit 3–7)

Kesalahan terbesar developer saat menggunakan AI adalah meminta AI membuatkan UI terlebih dahulu. Itu terbalik.
Dalam rekayasa perangkat lunak, data mendikte logika, dan logika mendikte antarmuka. Ketika Anda memiliki skema database yang valid dan terdefinisi dengan baik, 80% pekerjaan AI berikutnya tinggal melakukan inferensi tipe data.
Berikut adalah base template prisma/schema.prisma yang selalu saya gunakan sebagai pondasi SaaS multi-tenant:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
enum Role {
OWNER
MEMBER
}
enum SubscriptionStatus {
TRIAL
ACTIVE
PAST_DUE
CANCELED
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
memberships OrgMembership[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Organization {
id String @id @default(cuid())
name String
slug String @unique
subscriptionStatus SubscriptionStatus @default(TRIAL)
members OrgMembership[]
projects Project[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model OrgMembership {
id String @id @default(cuid())
role Role @default(MEMBER)
userId String
organizationId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([userId, organizationId])
@@index([userId])
@@index([organizationId])
}
model Project {
id String @id @default(cuid())
name String
description String?
organizationId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([organizationId])
}Eksekusi Prompt Chaining 1: Domain Modeling
Sekarang, berikan konteks schema di atas ke AI Anda (misal di Cursor Composer atau Chat) dengan prompt terarah berikut:
"Lihat `prisma/schema.prisma`. Saya ingin menambahkan fitur Feedback Board untuk SaaS ini. Setiap `Project` bisa memiliki banyak `FeedbackItem`. Setiap `FeedbackItem` memiliki title, content, status (OPEN, IN_PROGRESS, RESOLVED), upvotes count, dan dikirim oleh User. Tambahkan juga model `FeedbackUpvote` dengan constraint unique user-feedback. Kembalikan HANYA blok model Prisma baru yang valid lengkap dengan index dan onDelete cascade."
AI akan mengeluarkan ekstensi skema yang langsung klop dengan model yang sudah ada tanpa merusak relasi User maupun Organization.
Jalankan migrasi lokal:
npx prisma db pushTahap 3: Safe Server Actions & Type Contracts (Menit 7–11)

Langkah berikutnya adalah membangun lapisan mutasi data. Kita tidak ingin AI menulis try/catch manual di 20 file berbeda dengan handling error yang inkonsisten. Kita buat satu safe action client sebagai middleware.
Buat file src/lib/safe-action.ts:
import { createSafeActionClient } from "next-safe-action";
export class ActionError extends Error {
constructor(message: string) {
super(message);
this.name = "ActionError";
}
}
// Client dasar tanpa auth
export const actionClient = createSafeActionClient({
handleServerError(error) {
if (error instanceof ActionError) {
return error.message;
}
console.error("Unhandled Action Error:", error);
return "Terjadi kesalahan internal pada server.";
},
});
// Client terproteksi dengan mock/real session check
export const authActionClient = actionClient.use(async ({ next }) => {
// Ganti dengan session logic Anda (misal: auth() dari Auth.js)
const session = { user: { id: "user_mock_123", email: "adhinugroho@vour.dev" } };
if (!session?.user?.id) {
throw new ActionError("Anda harus login untuk melakukan aksi ini.");
}
return next({
ctx: {
userId: session.user.id,
userEmail: session.user.email,
},
});
});Penjelasan Implementasi:
createSafeActionClientmembungkus eksekusi server action agar tidak pernah melempar unhandled exception ke client yang bisa mengekspos detail database.- Middleware
.use()menyuntikkan konteksctx(userIddanuserEmail) secara otomatis ke setiap action turunan, sehingga kita tidak perlu memanggil auth helper berulang kali di setiap controller.
Eksekusi Prompt Chaining 2: Generating Mutation Actions
Sekarang, minta AI membuatkan server action lengkap dengan Zod schema:
"Berdasarkan `prisma/schema.prisma` dan utility `authActionClient` di `src/lib/safe-action.ts`, buatkan file `src/actions/feedback.ts`. Buat dua actions: `createFeedbackAction` dan `upvoteFeedbackAction`. Gunakan Zod untuk validasi input, pastikan projectId divalidasi, dan gunakan Prisma transaction untuk upvote."
Berikut kode yang dihasilkan dan harus kita verifikasi:
"use server";
import { z } from "zod";
import { prisma } from "@/lib/prisma";
import { authActionClient, ActionError } from "@/lib/safe-action";
import { revalidatePath } from "next/cache";
const createFeedbackSchema = z.object({
projectId: z.string().cuid("Project ID tidak valid."),
title: z.string().min(3, "Judul minimal 3 karakter.").max(100, "Judul maksimal 100 karakter."),
content: z.string().min(10, "Deskripsi minimal 10 karakter.").max(2000),
});
export const createFeedbackAction = authActionClient
.schema(createFeedbackSchema)
.action(async ({ parsedInput, ctx }) => {
const { projectId, title, content } = parsedInput;
const { userId } = ctx;
// Pastikan project ada
const project = await prisma.project.findUnique({
where: { id: projectId },
select: { id: true, organizationId: true },
});
if (!project) {
throw new ActionError("Project tidak ditemukan.");
}
const feedback = await prisma.feedbackItem.create({
data: {
title,
content,
projectId,
userId,
},
});
revalidatePath(`/projects/${projectId}/feedbacks`);
return { success: true, data: feedback };
});
const toggleUpvoteSchema = z.object({
feedbackId: z.string().cuid(),
});
export const toggleUpvoteAction = authActionClient
.schema(toggleUpvoteSchema)
.action(async ({ parsedInput, ctx }) => {
const { feedbackId } = parsedInput;
const { userId } = ctx;
return await prisma.$transaction(async (tx) => {
const existingUpvote = await tx.feedbackUpvote.findUnique({
where: {
userId_feedbackId: { userId, feedbackId },
},
});
if (existingUpvote) {
await tx.feedbackUpvote.delete({
where: { id: existingUpvote.id },
});
await tx.feedbackItem.update({
where: { id: feedbackId },
data: { upvotesCount: { decrement: 1 } },
});
return { upvoted: false };
} else {
await tx.feedbackUpvote.create({
data: { userId, feedbackId },
});
await tx.feedbackItem.update({
where: { id: feedbackId },
data: { upvotesCount: { increment: 1 } },
});
return { upvoted: true };
}
});
});Tips Senior: Selalu pastikan mutasi yang melibatkan increment/decrement counter seperti upvotes atau balance dibungkus dalam prisma.$transaction. AI sering lupa hal ini dan mengeksekusinya secara terpisah, yang memicu race condition saat traffic tinggi.Tahap 4: UI Scaffolding dengan shadcn & Type Inference (Menit 11–15)
Tahap terakhir adalah mengaitkan UI form ke Server Action. Karena kita sudah punya validasi Zod di backend, kita bisa memanfaatkan tipe inferensi Zod untuk form frontend tanpa menduplikasi definisi tipe data.
Pasang komponen UI yang dibutuhkan dari shadcn:
pnpm dlx shadcn@latest add button input textarea form card toastEksekusi Prompt Chaining 3: Form Component Generation
Prompt ke AI:
"Buat client component `src/components/create-feedback-form.tsx`. Gunakan `react-hook-form`, `@hookform/resolvers/zod`, komponen shadcn/ui (Form, Input, Textarea, Button), dan eksekusi `createFeedbackAction` menggunakan hook `useAction` dari `next-safe-action/hooks`. Tangani status isPending dan error toast."
Komponen yang dihasilkan:
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useAction } from "next-safe-action/hooks";
import { createFeedbackAction } from "@/actions/feedback";
import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
const formSchema = z.object({
title: z.string().min(3, "Judul minimal 3 karakter.").max(100),
content: z.string().min(10, "Deskripsi minimal 10 karakter.").max(2000),
});
type FormValues = z.infer<typeof formSchema>;
interface CreateFeedbackFormProps {
projectId: string;
onSuccess?: () => void;
}
export function CreateFeedbackForm({ projectId, onSuccess }: CreateFeedbackFormProps) {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
content: "",
},
});
const { execute, isExecuting } = useAction(createFeedbackAction, {
onSuccess: ({ data }) => {
if (data?.success) {
form.reset();
onSuccess?.();
}
},
onError: ({ error }) => {
alert(error.serverError || "Gagal membuat feedback.");
},
});
function onSubmit(values: FormValues) {
execute({
projectId,
title: values.title,
content: values.content,
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Judul Masukan</FormLabel>
<FormControl>
<Input placeholder="Contoh: Tambahkan dark mode..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>Detail Deskripsi</FormLabel>
<FormControl>
<Textarea
placeholder="Jelaskan kebutuhan fitur atau bug yang dialami..."
rows={4}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full" disabled={isExecuting}>
{isExecuting ? "Menyimpan..." : "Kirim Masukan"}
</Button>
</form>
</Form>
);
}Detail Implementasi:
useAction(createFeedbackAction)mengikat state eksekusi (isExecuting,result,error) secara reaktif ke lifecycle React.- Form tidak memerlukan handling
e.preventDefault()manual karena ditangani oleh wrapperform.handleSubmit. - Validasi berjalan ganda: secara instan di sisi browser (via
zodResolver) dan secara absolut di sisi server (viaauthActionClient.schema()).
Anti-Pattern yang Harus Dihindari
Saat mengintegrasikan AI ke dalam daily engineering workflow, ada tiga jebakan utama yang sering membuat developer junior membuang-buang waktu:
- Context Window Pollution: Memasukkan 50 file ke dalam context prompt AI. Semakin banyak noise yang tidak relevan (seperti file config tailwind, CSS global, atau aset gambar), semakin tinggi kemungkinan LLM membuat halusinasi kode. Cukup lampirkan
schema.prismadan file action yang relevan. - Accepting Without Verification: Menerima kode AI tanpa membaca relasi foreign key dan logic auth. AI sering lupa menambahkan
userIdcheck saat mutasiUPDATEatauDELETE, yang berpotensi menimbulkan celah IDOR (Insecure Direct Object Reference). - Over-Prompting UI Layout: Meminta AI mendesain layout dashboard yang kompleks dari nol lewat teks. Jauh lebih cepat membuat struktur flex/grid sendiri dengan Tailwind, lalu meminta AI mengisi data binding komponen di dalamnya.
Kesimpulan & Action Items
AI tidak menggantikan pemahaman fundamental software architecture kita. Justru sebaliknya: semakin kuat pemahaman Anda tentang data modeling, abstraction layers, dan type system, semakin dahsyat output yang bisa dihasilkan oleh AI tooling Anda.
Dengan membagi workflow menjadi 4 tahapan deterministik:
- Pasang System Rules & Scaffolding Minimalis.
- Susun Skema Database Relasional (Prisma) sebagai Single Source of Truth.
- Generate Safe Server Actions dengan kontrak Zod.
- Binding UI Komponen via Hook & Safe Actions.
Anda memangkas waktu setup repetitif dari 2 hari menjadi 15 menit tanpa mengorbankan kualitas arsitektur kode.
Action Items untuk Anda Hari Ini:
- Simpan template
.cursorrulesdi atas ke dalam catatan atau gist lokal Anda. - Buat satu repositori template kosong berisi skema Prisma multi-tenant di GitHub.
- Coba ikuti urutan 4 tahap prompt chaining ini untuk fitur micro-SaaS Anda berikutnya. Ukur berapa menit yang Anda hemat.
Butuh penyesuaian khusus untuk project Anda?
Jika situasi operasional atau arsitektur sistem bisnis Anda membutuhkan solusi kustom, diskusikan langsung bersama tim engineer kami. Anda juga bisa melihat rincian layanan vour.dev atau menghitung estimasi biaya project lebih dulu.
Mulai Project