Monity โ€“ Finance Tracker

Bukti Pemenuhan 17 Kriteria Unjuk Kerja (KUK) โ€” BNSP Skema J.620100 Web Developer

๐Ÿ‘ค Peserta: Moch Virgiawan C ๐Ÿ—“๏ธ Februari 2026 ๐Ÿ“‹ Skema: J.620100 Web Developer โœ… 17 / 17 KUK Terpenuhi

Tentang Aplikasi

Monity adalah aplikasi finance tracker personal berbasis web yang dibangun menggunakan Next.js 14 (App Router), TypeScript, Drizzle ORM, PostgreSQL, dan Google Gemini AI. Aplikasi ini memungkinkan pengguna mencatat transaksi keuangan, mengelola rekening, mengkategorikan pengeluaran, serta mendapatkan insight berbasis AI.

Next.js 14 TypeScript Drizzle ORM PostgreSQL (Supabase) NextAuth.js v5 Google Gemini AI Zod Tailwind CSS Framer Motion React Query
Ringkasan Pemenuhan KUK
No. KUK Nama KUK Fitur di Monity Status
J.620100.001 Analisis Tools Pemilihan Next.js, Drizzle, Zod, Gemini AI โœ… Terpenuhi
J.620100.003 Framework / Library Next.js App Router + Drizzle ORM โœ… Terpenuhi
J.620100.006 UX Design Dark/Light Mode, Framer Motion, Responsive โœ… Terpenuhi
J.620100.017 Kode Terstruktur Modular: api/, components/, lib/, app/ โœ… Terpenuhi
J.620100.018 OOP / Type-safe TypeScript interface, class Drizzle, Zod infer โœ… Terpenuhi
J.620100.019 Library Eksternal bcryptjs, next-auth, @google/generative-ai โœ… Terpenuhi
J.620100.020 SQL / Query DB Drizzle ORM query builder dengan filter kompleks โœ… Terpenuhi
J.620100.021 Akses Database PostgreSQL via Drizzle + Supabase connection pool โœ… Terpenuhi
J.620100.022 Algoritma Auto-categorize, percentage change, monthly trend โœ… Terpenuhi
J.620100.024 Migrasi Database Drizzle Kit push/migrate + schema versioning โœ… Terpenuhi
J.620100.025 Debugging try/catch, console.error, status code HTTP โœ… Terpenuhi
J.620100.030 Multimedia / Aset SVG icons (Lucide), Gradient UI, Tailwind CSS โœ… Terpenuhi
J.620100.032 Code Review Zod validation layer, type checking TypeScript โœ… Terpenuhi
J.620100.036 Pengujian Error handling + Zod schema test via safeParse() โœ… Terpenuhi
J.620100.044 Alert / Notifikasi HTTP status codes + JSON error response โœ… Terpenuhi
J.620100.045 Monitor Sistem Gemini AI insight (spending_alert, monthly trend) โœ… Terpenuhi
J.620100.047 Pembaruan PL Dependency management npm, env config, Supabase โœ… Terpenuhi
Detail Bukti Per KUK
J.620100.001
Analisis & Pemilihan Tools Pengembangan
Menganalisis dan memilih tools/framework yang sesuai kebutuhan proyek
โœ… Terpenuhi

Pemilihan stack teknologi didasarkan pada analisis kebutuhan: Next.js dipilih karena mendukung SSR dan API route dalam satu project; Drizzle ORM karena type-safe dan ringan; Supabase karena menyediakan PostgreSQL managed; Zod karena validasi runtime yang konsisten dengan TypeScript.

๐Ÿ“ฆ package.json โ€“ Stack Teknologi Utama
"dependencies": {
  "next": "^14",           // Framework SSR + API Route
  "drizzle-orm": "^0.x",  // ORM type-safe untuk PostgreSQL
  "@google/generative-ai": "^0.x", // AI โ€” Gemini
  "zod": "^3",             // Validasi schema runtime
  "next-auth": "^5",       // Autentikasi modern
  "bcryptjs": "^2",        // Hashing password
  "framer-motion": "^11"  // Animasi UI halus
}
J.620100.003
Penggunaan Framework / Library
Menggunakan framework sesuai best practice
โœ… Terpenuhi

Next.js App Router digunakan dengan pattern route.ts sebagai API handler. Data diakses melalui Drizzle ORM dengan query builder yang type-safe.

๐Ÿ“„ src/app/api/transactions/route.ts
Contoh: Next.js API Route Handler
export async function GET(request: Request) {
  const session = await auth();
  if (!session?.user?.id) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }
  const result = await db.query.transactions.findMany({
    where: and(...conditions),
    with: { account: true, category: true },
    orderBy: [desc(transactions.transactionDate)],
  });
  return NextResponse.json({ transactions: result });
}
J.620100.006
UX Design & Antarmuka Pengguna
Merancang UI/UX yang responsif dan estetis
โœ… Terpenuhi

UI menggunakan Tailwind CSS dengan dark/light mode, Framer Motion untuk animasi micro-interaction, sidebar responsif (mobile hamburger menu), dan komponen reusable. Warna disesuaikan per tema secara kontekstual.

๐Ÿ“„ src/components/layout/sidebar.tsx
Contoh: Animasi + Dark Mode + Responsive
// Framer Motion hover animation
<motion.div whileHover={{ x: 4 }} whileTap={{ scale: 0.98 }}>
  <Link className={cn(
    "flex items-center gap-3 rounded-xl px-4 py-3 transition-all",
    isActive
      ? "bg-blue-600 text-white shadow-md"
      : "text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-900"
  )}>
    <Icon /> <span>{item.title}</span>
  </Link>
</motion.div>

// ThemeToggle (light/dark mode)
const { theme, setTheme } = useTheme()
setTheme(theme === "light" ? "dark" : "light")
J.620100.017
Penulisan Kode Terstruktur
Kode modular, readable, dan mengikuti konvensi
โœ… Terpenuhi

Proyek diorganisir dalam struktur modular Next.js App Router: app/ (halaman & API), components/ (UI reusable), lib/ (logika bisnis: db, ai, auth, utils, validations), dan types/.

src/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ (auth)/         // Login, Register
โ”‚   โ”œโ”€โ”€ (dashboard)/    // Dashboard, Transactions, Accounts, Insights
โ”‚   โ””โ”€โ”€ api/            // REST API handlers
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ layout/         // Sidebar
โ”‚   โ”œโ”€โ”€ dashboard/      // Dashboard cards
โ”‚   โ””โ”€โ”€ ui/             // Button, Card, dll
โ””โ”€โ”€ lib/
    โ”œโ”€โ”€ db/             // Drizzle schema + koneksi
    โ”œโ”€โ”€ ai/             // Gemini integration
    โ”œโ”€โ”€ auth/           // NextAuth config
    โ”œโ”€โ”€ validations.ts  // Semua Zod schema
    โ””โ”€โ”€ utils.ts        // Helper functions
J.620100.018
Pemrograman Berorientasi Objek (OOP / Type-safe)
Penggunaan interface, class, enkapsulasi, dan tipe data
โœ… Terpenuhi

Apakah OOP di TypeScript menggunakan interface? โ€” Ya, interface adalah bagian inti dari OOP TypeScript. Interface berfungsi seperti blueprint/kontrak (mirip abstract class di PHP/Java): mendefinisikan struktur data tanpa implementasi, memaksa setiap objek yang menggunakannya memiliki properti yang sesuai. Di Monity, OOP diwujudkan melalui 4 pilar: interface (kontrak data), class (instantiasi objek AI), encapsulation (Zod schema memproteksi validasi), dan type inference (type-safe dari Drizzle schema).

โ‘  interface โ€” Blueprint / Kontrak Data (src/lib/ai/gemini.ts)
// interface = mendefinisikan STRUKTUR data (seperti abstract class PHP)
export interface FinancialSummary {
  totalIncome: number;      // wajib ada
  totalExpense: number;     // wajib ada
  balance: number;
  topExpenseCategories: { category: string; amount: number }[];
  monthlyComparison?: {          // opsional (tanda ?)
    currentMonth: number;
    previousMonth: number;
    percentageChange: number;
  };
}

// Fungsi menerima kontrak interface โ€” tidak bisa input sembarangan
async function generateFinancialInsight(summary: FinancialSummary, type: string) {
  // TypeScript error jika summary tidak punya totalIncome, dsb.
}
โ‘ก class โ€” Instantiasi Objek (src/lib/ai/gemini.ts)
// class = OOP klasik, membuat objek dari blueprint
import { GoogleGenerativeAI } from "@google/generative-ai";

// new GoogleGenerativeAI() โ†’ instantiasi class seperti PHP: new GeminiClient()
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);

// Memanggil method dari instance (encapsulation)
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash-lite" });
const result = await model.generateContent(prompt);
โ‘ข type inference โ€” OOP Drizzle Schema (src/lib/db/schema.ts)
// Type otomatis di-generate dari definisi tabel (type-safe OOP)
export type Transaction = typeof transactions.$inferSelect;   // untuk SELECT
export type NewTransaction = typeof transactions.$inferInsert; // untuk INSERT

// Type dari Zod schema (encapsulation validasi)
export type CreateTransactionInput = z.infer<typeof createTransactionSchema>;
J.620100.019
Penggunaan Library Eksternal
Mengintegrasikan library pihak ketiga secara tepat
โœ… Terpenuhi

Beberapa library eksternal diintegrasikan dengan tepat sesuai kebutuhan masing-masing: bcryptjs untuk hashing, NextAuth untuk session, Gemini AI untuk insight cerdas.

๐Ÿ“„ src/app/api/auth/register/route.ts & src/lib/ai/gemini.ts
// bcryptjs โ€” hashing password sebelum simpan ke DB
import bcrypt from "bcryptjs";
const hashedPassword = await bcrypt.hash(password, 10);

// @google/generative-ai โ€” Google Gemini AI
import { GoogleGenerativeAI } from "@google/generative-ai";
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash-lite" });
const result = await model.generateContent(prompt);

// next-auth โ€” session autentikasi
const session = await auth();
if (!session?.user?.id) return NextResponse.json({error:"Unauthorized"},{status:401});
J.620100.020
Query SQL / Database
Membuat query data dengan filter, join, sort
โœ… Terpenuhi

Query kompleks menggunakan Drizzle ORM sebagai query builder type-safe yang di-compile menjadi SQL asli: WHERE multi kondisi, LEFT JOIN antar tabel, ORDER BY, LIMIT/OFFSET, dan UPDATE dengan SQL expression.

๐Ÿ“„ src/app/api/transactions/route.ts
Drizzle ORM (TypeScript)
// SELECT transactions dengan filter + join + sort + paginasi
const result = await db.query.transactions.findMany({
  where: and(
    eq(transactions.userId, session.user.id),
    gte(transactions.transactionDate, query.startDate),
    lte(transactions.transactionDate, query.endDate)
  ),
  with: { account: true, category: true }, // LEFT JOIN otomatis
  orderBy: [desc(transactions.transactionDate)],
  limit: query.limit,   // default 50
  offset: query.offset, // paginasi
});

// UPDATE balance rekening setelah transaksi masuk
await db.update(financeAccounts)
  .set({ balance: sql`${financeAccounts.balance} + ${balanceChange}` })
  .where(eq(financeAccounts.id, accountId));
๐Ÿ—„๏ธ SQL Asli yang Di-generate (PostgreSQL)
-- SELECT: ambil transaksi dengan JOIN ke tabel account & category
SELECT
  t.id, t.type, t.amount, t.description, t.merchant, t.transaction_date,
  a.id  AS account_id, a.name AS account_name, a.type AS account_type,
  c.id  AS category_id, c.name AS category_name, c.color
FROM "transaction" t
  LEFT JOIN "finance_account" a ON t.account_id = a.id
  LEFT JOIN "category" c ON t.category_id = c.id
WHERE
  t.user_id = 'user-uuid-here'
  AND t.transaction_date >= '2026-01-01'
  AND t.transaction_date <= '2026-01-31'
ORDER BY t.transaction_date DESC, t.created_at DESC
LIMIT 50 OFFSET 0;

-- UPDATE: kurangi atau tambah saldo rekening setelah transaksi
UPDATE "finance_account"
SET
  balance = balance + 50000,  -- +income / -expense
  updated_at = NOW()
WHERE id = 'account-uuid-here';

-- INSERT: tambah transaksi baru
INSERT INTO "transaction" (id, user_id, account_id, category_id, type, amount, transaction_date)
VALUES (gen_random_uuid(), 'user-uuid', 'account-uuid', 'category-uuid', 'expense', 150000, '2026-01-15')
RETURNING *;
J.620100.021
Koneksi dan Akses Database
Koneksi ke DB melalui library/ORM
โœ… Terpenuhi

Koneksi ke PostgreSQL (Supabase) menggunakan postgres.js dengan connection pool mode. Instance Drizzle dibuat sekali dan di-export untuk digunakan di semua API route.

๐Ÿ“„ src/lib/db/index.ts
import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

const connectionString = process.env.DATABASE_URL!; // Supabase URL

// prepare:false = Transaction pool mode (Supabase)
const sql = postgres(connectionString, { prepare: false });

// Instance tunggal, dipakai di semua API route
export const db = drizzle(sql, { schema });
J.620100.022
Penerapan Algoritma
Logika pemrograman: loop, kondisi, kalkulasi
โœ… Terpenuhi

Tiga algoritma utama: (1) auto-kategorisasi berdasarkan keyword matching, (2) kalkulasi persentase perubahan pengeluaran bulanan, (3) agregasi data trend 6 bulan.

๐Ÿ“„ src/app/api/transactions/route.ts & src/lib/utils.ts
// Algoritma 1: Auto-kategorisasi transaksi berdasarkan merchant
async function autoCategorize(merchant: string, type: string, userId: string) {
  const allCategories = await db.query.categories.findMany(...);
  const merchantLower = merchant.toLowerCase();
  for (const category of allCategories) {
    const keywords = category.keywords.split(",").map(k => k.trim().toLowerCase());
    if (keywords.some(kw => merchantLower.includes(kw))) return category;
  }
  return null;
}

// Algoritma 2: Hitung persentase perubahan bulanan
export function calculatePercentageChange(current: number, previous: number) {
  if (previous === 0) return current > 0 ? 100 : 0;
  return ((current - previous) / previous) * 100;
}

// Algoritma 3: Loop 6 bulan terakhir untuk trend chart
for (let i = months - 1; i >= 0; i--) {
  const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
  // ... ambil data tiap bulan, hitung income & expense
}
J.620100.024
Migrasi Database
Membuat dan menjalankan migrasi schema DB
โœ… Terpenuhi

Drizzle Kit digunakan untuk generate dan menjalankan migrasi. Schema TypeScript di-compile menjadi DDL SQL lalu di-push ke Supabase PostgreSQL menggunakan perintah npx drizzle-kit push.

Drizzle Schema (TypeScript) โ€” src/lib/db/schema.ts
export const transactions = pgTable("transaction", {
  id:              text("id").primaryKey().$defaultFn(() => crypto.randomUUID()),
  userId:          text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
  accountId:       text("account_id").notNull().references(() => financeAccounts.id),
  categoryId:      text("category_id").references(() => categories.id, { onDelete: "set null" }),
  type:            text("type").notNull(),
  amount:          decimal("amount", { precision: 15, scale: 2 }).notNull(),
  description:     text("description"),
  merchant:        text("merchant"),
  transactionDate: date("transaction_date", { mode: "date" }).notNull(),
  createdAt:       timestamp("created_at").defaultNow().notNull(),
  updatedAt:       timestamp("updated_at").defaultNow().notNull(),
});
๐Ÿ—„๏ธ SQL DDL Asli yang Di-generate oleh Drizzle Kit (PostgreSQL)
-- Hasil: npx drizzle-kit generate โ†’ file .sql berikut
CREATE TABLE IF NOT EXISTS "transaction" (
  "id"               TEXT        PRIMARY KEY,
  "user_id"          TEXT        NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
  "account_id"       TEXT        NOT NULL REFERENCES "finance_account"(id) ON DELETE CASCADE,
  "category_id"      TEXT        REFERENCES "category"(id) ON DELETE SET NULL,
  "type"             TEXT        NOT NULL,
  "amount"           DECIMAL(15,2) NOT NULL,
  "description"      TEXT,
  "merchant"         TEXT,
  "transaction_date" DATE        NOT NULL,
  "created_at"       TIMESTAMP   DEFAULT NOW() NOT NULL,
  "updated_at"       TIMESTAMP   DEFAULT NOW() NOT NULL
);

-- Perintah yang dijalankan developer
-- $ npx drizzle-kit push     โ† langsung push ke Supabase
-- $ npx drizzle-kit generate โ† generate file .sql migrasi
-- $ npx drizzle-kit migrate  โ† jalankan file migrasi
J.620100.025
Debugging & Penanganan Error
Tracing bug, error handling, status code
โœ… Terpenuhi

Setiap API route dibungkus try/catch dengan log ke console.error dan response JSON berisi pesan error + HTTP status code yang tepat (400, 401, 404, 409, 500).

๐Ÿ“„ src/app/api/auth/register/route.ts
try {
  const parsed = registerSchema.safeParse(body);
  if (!parsed.success)
    return NextResponse.json({error:"Validation failed"}, {status: 400});

  if (existingUser)
    return NextResponse.json({error:"Email sudah terdaftar"}, {status: 409});

  return NextResponse.json({message:"Registrasi berhasil", user: newUser}, {status: 201});
} catch (error) {
  console.error("Registration error:", error); // Log untuk debugging
  return NextResponse.json({error:"Terjadi kesalahan server"}, {status: 500});
}
J.620100.030
Pengelolaan Multimedia / Aset
Penggunaan icon, gambar, dan aset visual
โœ… Terpenuhi

Icon SVG dari library Lucide React digunakan di seluruh UI. Gradient background dan warna dinamis per akun/kategori dikonfigurasi melalui properti database (icon, color).

๐Ÿ“„ src/components/layout/sidebar.tsx & src/lib/db/schema.ts
// Lucide React icons (SVG)
import { LayoutDashboard, Receipt, PieChart, Wallet, Target, LogOut } from "lucide-react"

// Icon & warna tersimpan di DB per akun/kategori
export const financeAccounts = pgTable("finance_account", {
  icon: text("icon"),   // e.g. "wallet", "credit-card"
  color: text("color"), // e.g. "#3b82f6"
});

// Gradient di header sidebar
// bg-gradient-to-tr from-blue-600 to-indigo-600
J.620100.032
Code Review & Standar Kode
Validasi kode, konsistensi, dan clean code
โœ… Terpenuhi

Zod schema berfungsi sebagai lapisan validasi/review otomatis: setiap input dari client divalidasi sebelum diproses. TypeScript memberikan static type checking saat development.

๐Ÿ“„ src/lib/validations.ts
// Zod schema = definisi aturan + validasi sekaligus
export const createTransactionSchema = z.object({
  accountId: z.string().min(1, "Akun wajib dipilih"),
  type: z.enum(["income", "expense"]),
  amount: z.coerce.number().positive("Nominal harus lebih dari 0"),
  transactionDate: z.coerce.date(),
  categoryId: z.string().optional(),
  description: z.string().optional(),
});

// Digunakan di API โ†’ validasi sebelum insert ke DB
const parsed = createTransactionSchema.safeParse(body);
if (!parsed.success) return NextResponse.json({error: "Validation failed"}, {status: 400});
J.620100.036
Pengujian Perangkat Lunak
Testing fungsional dan validasi input
โœ… Terpenuhi

Pengujian dilakukan melalui: (1) validasi runtime Zod dengan safeParse() sebagai unit test fungsional, (2) session guard (unauthorized check) sebagai integration test, (3) error handling di setiap endpoint sebagai regression test.

// Test case 1: Input tidak valid โ†’ 400 Bad Request
// POST /api/transactions body: { amount: -100 }
// โ†’ Zod: "Nominal harus lebih dari 0" โ†’ { error: "Validation failed" }

// Test case 2: Tidak login โ†’ 401 Unauthorized
// GET /api/transactions tanpa session
// โ†’ { error: "Unauthorized" }

// Test case 3: Email sudah ada โ†’ 409 Conflict
// POST /api/auth/register dengan email yang sudah terdaftar
// โ†’ { error: "Email sudah terdaftar" }

// Zod safeParse() โ€” simulasi unit test validasi
const result = createTransactionSchema.safeParse({ amount: -100 });
console.log(result.success); // false โ€” validasi berjalan
J.620100.044
Alert & Notifikasi Sistem
Informasi status proses kepada pengguna
โœ… Terpenuhi

Sistem merespons setiap permintaan dengan status HTTP yang informatif dan pesan yang jelas. Di frontend, toast notification / alert ditampilkan berdasarkan response API.

// Tabel kode status yang digunakan di Monity:
// 200 OK        โ†’ Data berhasil diambil
// 201 Created   โ†’ Transaksi/Akun/Insight berhasil dibuat
// 400 Bad Req   โ†’ Validasi gagal (Zod error)
// 401 Unauth    โ†’ Belum login
// 404 Not Found โ†’ Akun/item tidak ditemukan
// 409 Conflict  โ†’ Email sudah terdaftar
// 500 Server Err โ†’ Error tidak terduga

// Contoh notifikasi spesifik di insight generate:
if (summary.totalIncome === 0 && summary.totalExpense === 0) {
  return NextResponse.json(
    { error: "Tidak ada data transaksi untuk periode ini" },
    { status: 400 }
  );
}
J.620100.045
Monitoring Sistem
Memantau kondisi dan performa aplikasi
โœ… Terpenuhi

Fitur Insight AI berfungsi sebagai monitoring keuangan: spending_alert mendeteksi lonjakan pengeluaran, monthly_summary merangkum kondisi keuangan, dan monthly trend memantau pola 6 bulan terakhir.

๐Ÿ“„ src/lib/ai/gemini.ts & src/app/api/insights/generate/route.ts
// Monitoring pengeluaran: deteksi perubahan signifikan
case "spending_alert":
  const changeInfo = comparison
    ? `Pengeluaran bulan ini ${comparison.percentageChange > 0 ? "naik" : "turun"}
       ${Math.abs(comparison.percentageChange).toFixed(1)}% dibanding bulan lalu.`
    : "";

// Trend monitoring 6 bulan terakhir
const monthlyTrend = await getMonthlyTrend(session.user.id, 6);
// โ†’ [{month:"Jan",income:5000000,expense:3000000}, ...]
J.620100.047
Pembaruan Perangkat Lunak
Manajemen dependensi, konfigurasi, dan update
โœ… Terpenuhi

Manajemen dependensi menggunakan npm dengan package.json. Konfigurasi environment (API key, DB URL) dikelola via .env tanpa hardcode. Supabase memungkinkan update schema database tanpa downtime.

// .env.local โ€” konfigurasi environment (tidak di-commit)
DATABASE_URL="postgresql://..."   // Supabase connection
GEMINI_API_KEY="AIza..."           // Google Gemini API
NEXTAUTH_SECRET="..."               // NextAuth secret

// Update dependensi:
// $ npm update        โ†’ update semua package
// $ npm audit fix     โ†’ patch security vulnerability
// $ npx drizzle-kit push โ†’ update schema DB

// Gemini model mudah diperbarui di satu tempat:
const geminiModel = genAI.getGenerativeModel({
  model: "gemini-2.5-flash-lite", // Ganti versi di sini
});
๐Ÿ“ Soal Latihan Studi Kasus & Jawaban

โ„น๏ธ Panduan Latihan

Soal berbasis kompetensi SKKNI J.620100 Web Developer. Klik "Lihat Jawaban" pada setiap soal untuk melihat pembahasan lengkap. Jawaban dapat ditampilkan langsung tanpa perlu akses khusus.

Total Soal10
Mudah3
Sedang4
Sulit3
Soal 1 J.620100.003.01 Sedang
Framework & Library PHP
Anda ditugaskan membangun sistem manajemen inventaris untuk sebuah toko online menggunakan PHP. a) Jelaskan perbedaan antara menggunakan framework (seperti Laravel) vs membangun dari awal (raw PHP) untuk proyek ini. b) Sebutkan minimal 3 keuntungan menggunakan Laravel untuk proyek skala menengah ke atas. c) Jelaskan konsep MVC (Model-View-Controller) dalam Laravel dengan contoh untuk fitur "daftar produk".
โœ… Jawaban / Pembahasan

a) Framework vs Raw PHP:

  • Raw PHP: Semua ditulis sendiri โ€” routing, koneksi DB, validasi, keamanan. Lebih fleksibel tapi lambat dikembangkan dan rentan bug.
  • Laravel: Menyediakan struktur bawaan MVC, ORM (Eloquent), routing, middleware, autentikasi, migrasi DB, dan lainnya secara siap pakai.

b) 3 Keuntungan Laravel:

  1. Eloquent ORM โ€” query database menggunakan method PHP tanpa SQL manual, type-safe.
  2. Artisan CLI โ€” generate controller, model, migration dengan satu perintah (php artisan make:model Product -mcr).
  3. Security built-in โ€” CSRF protection, SQL injection prevention, bcrypt hashing sudah disediakan secara default.

c) MVC untuk fitur "Daftar Produk":

// Model: app/Models/Product.php
class Product extends Model {
    protected $fillable = ['name', 'price', 'stock'];
}

// Controller: app/Http/Controllers/ProductController.php
class ProductController extends Controller {
    public function index() {
        $products = Product::orderBy('name')->paginate(10);
        return view('products.index', compact('products'));
    }
}

// Route: routes/web.php
Route::get('/products', [ProductController::class, 'index']);

// View: resources/views/products/index.blade.php
@foreach($products as $p)
    <tr><td>{{ $p->name }}</td><td>{{ $p->price }}</td></tr>
@endforeach
{{ $products->links() }}   {{-- Pagination otomatis --}}
Soal 2 J.620100.017.02 Sedang
Pemrograman Terstruktur โ€” Routing & Middleware
Sebuah aplikasi web memiliki halaman yang harus dilindungi: hanya pengguna yang sudah login dan berstatus 'approved' yang dapat mengaksesnya. a) Jelaskan apa itu middleware dalam Laravel dan bagaimana cara kerjanya. b) Tulis middleware EnsureApproved yang memeriksa status user. c) Daftarkan middleware tersebut pada route /dashboard/admin.
โœ… Jawaban / Pembahasan

a) Middleware adalah lapisan yang berjalan sebelum request masuk ke controller. Digunakan untuk autentikasi, otorisasi, logging, dsb.

b) Middleware EnsureApproved:

// app/Http/Middleware/EnsureApproved.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;

class EnsureApproved {
    public function handle(Request $request, Closure $next) {
        // Cek sudah login
        if (!auth()->check()) {
            return redirect('/login');
        }
        // Cek status approved
        if (auth()->user()->status !== 'approved') {
            abort(403, 'Akun belum disetujui admin.');
        }
        return $next($request); // Lanjut ke controller
    }
}

c) Daftarkan di Route:

// routes/web.php
Route::get('/dashboard/admin', [AdminController::class, 'index'])
    ->middleware(['auth', 'ensure.approved']);

// bootstrap/app.php (Laravel 11) atau Kernel.php (Laravel 10)
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias(['ensure.approved' => EnsureApproved::class]);
})
Soal 3 J.620100.018.02 Sedang
Pemrograman Berorientasi Objek (OOP)
Anda diminta membuat sistem order produk menggunakan pendekatan OOP di PHP. a) Jelaskan 4 pilar OOP (Encapsulation, Inheritance, Polymorphism, Abstraction) dengan contoh kasus sistem order. b) Buatlah class Order dengan property dan method yang menerapkan enkapsulasi. c) Buatlah class OnlineOrder yang meng-extend Order (inheritance).
โœ… Jawaban / Pembahasan

a) 4 Pilar OOP:

  • Encapsulation: Property disembunyikan (private/protected), diakses via getter/setter. Contoh: $order->getTotal() bukan $order->total secara langsung.
  • Inheritance: Class anak mewarisi class induk. Contoh: OnlineOrder extends Order.
  • Polymorphism: Method yang sama berperilaku beda di class berbeda. Contoh: calculateShipping() beda di OnlineOrder vs StoreOrder.
  • Abstraction: Menyembunyikan detail implementasi. Contoh: interface/abstract class Payable dengan method pay().

b) Class Order dengan Enkapsulasi:

class Order {
    private int $id;
    private array $items = [];
    private float $total = 0;
    private string $status = 'pending';

    public function __construct(int $id) {
        $this->id = $id;
    }

    public function addItem(string $name, float $price, int $qty): void {
        $this->items[] = compact('name', 'price', 'qty');
        $this->total += $price * $qty;
    }

    public function getTotal(): float { return $this->total; }
    public function getStatus(): string { return $this->status; }
    public function approve(): void { $this->status = 'approved'; }
}

c) Class OnlineOrder (Inheritance):

class OnlineOrder extends Order {
    private string $shippingAddress;
    private string $courier;

    public function __construct(int $id, string $address, string $courier) {
        parent::__construct($id); // Panggil constructor induk
        $this->shippingAddress = $address;
        $this->courier = $courier;
    }

    public function calculateShipping(): float {
        // Override: ongkir khusus online order
        return $this->courier === 'JNE' ? 15000 : 20000;
    }

    public function getFinalTotal(): float {
        return $this->getTotal() + $this->calculateShipping();
    }
}
Soal 4 J.620100.020.02 Sedang
SQL & Akses Database
Diberikan skema: โ€ข products (id, product_code, product_name, price) โ€ข orders (id_order, tgl_order, customer, grand_total) โ€ข order_details (id, id_order, product_id, qty, harga_saat_beli, total) a) Tulis query SQL untuk menampilkan 5 produk terlaris berdasarkan total qty terjual. b) Tulis query untuk menampilkan total pendapatan per bulan di tahun 2025. c) Tulis query untuk menampilkan order detail beserta nama produknya.
โœ… Jawaban / Pembahasan

a) 5 Produk Terlaris:

SELECT
    p.product_name,
    p.product_code,
    SUM(od.qty)   AS total_terjual,
    SUM(od.total) AS total_pendapatan
FROM order_details od
JOIN products p ON od.product_id = p.id
GROUP BY p.id, p.product_name, p.product_code
ORDER BY total_terjual DESC
LIMIT 5;

b) Total Pendapatan per Bulan 2025:

SELECT
    MONTH(tgl_order)      AS bulan,
    MONTHNAME(tgl_order)  AS nama_bulan,
    COUNT(id_order)       AS jumlah_order,
    SUM(grand_total)      AS total_pendapatan
FROM orders
WHERE YEAR(tgl_order) = 2025
GROUP BY MONTH(tgl_order), MONTHNAME(tgl_order)
ORDER BY bulan;

c) Order Detail + Nama Produk:

SELECT
    o.id_order,
    o.customer,
    o.tgl_order,
    p.product_name,
    od.qty,
    od.harga_saat_beli,
    od.total
FROM order_details od
JOIN orders  o ON od.id_order    = o.id_order
JOIN products p ON od.product_id = p.id
ORDER BY o.tgl_order DESC;
Soal 5 J.620100.024.02 Mudah
Migrasi Database
Anda perlu menambahkan fitur review produk. Tabel product_reviews perlu dibuat dengan kolom: id, product_id (FK), user_id (FK), rating (1-5), review_text, created_at. a) Tulis migration Laravel untuk membuat tabel ini. b) Tulis command Artisan untuk menjalankan migrasi. c) Jelaskan perbedaan antara migrate, migrate:rollback, dan migrate:fresh.
โœ… Jawaban / Pembahasan

a) Migration Laravel:

// database/migrations/xxxx_create_product_reviews_table.php
public function up(): void {
    Schema::create('product_reviews', function (Blueprint $table) {
        $table->id();
        $table->foreignId('product_id')
              ->constrained('products')
              ->onDelete('cascade');
        $table->foreignId('user_id')
              ->constrained('users')
              ->onDelete('cascade');
        $table->tinyInteger('rating')
              ->unsigned()
              ->comment('Nilai 1-5');
        $table->text('review_text')->nullable();
        $table->timestamps(); // created_at + updated_at otomatis
    });
}

public function down(): void {
    Schema::dropIfExists('product_reviews');
}

b) Artisan Commands:

php artisan make:migration create_product_reviews_table
php artisan migrate

c) Perbedaan Perintah Migrate:

  • migrate โ€” Jalankan semua migrasi yang belum dijalankan (aman, non-destruktif).
  • migrate:rollback โ€” Batalkan batch migrasi terakhir (jalankan method down()).
  • migrate:fresh โ€” Hapus semua tabel lalu jalankan ulang semua migrasi dari awal. โš ๏ธ Data hilang! Hanya untuk development.
Soal 6 J.620100.022.02 Sulit
Algoritma & Struktur Data
Sistem order menerima request yang sangat tinggi dan perlu dioptimasi. a) Jelaskan perbedaan kompleksitas waktu O(1), O(n), O(nยฒ) dengan contoh dalam konteks pencarian produk. b) Bagaimana cara mengimplementasikan cache sederhana untuk daftar produk di Laravel? c) Tulis fungsi PHP untuk mencari produk berdasarkan keyword secara efisien menggunakan binary search (asumsikan data sudah terurut).
โœ… Jawaban / Pembahasan

a) Kompleksitas Waktu:

  • O(1) โ€” Konstan, tidak bergantung jumlah data. Contoh: akses produk lewat ID di array asosiatif ($products[$id]).
  • O(n) โ€” Linear, sebanding jumlah data. Contoh: loop seluruh daftar produk untuk cari nama (foreach).
  • O(nยฒ) โ€” Kuadratik, sangat lambat untuk data besar. Contoh: nested loop membandingkan semua produk satu per satu (bubble sort manual).

b) Cache Produk di Laravel:

// Cache selama 10 menit
$products = Cache::remember('all_products', 600, function () {
    return Product::orderBy('name')->get();
});

// Hapus cache saat ada update produk
Cache::forget('all_products');

// Atau gunakan cache tags (Redis)
Cache::tags(['products'])->remember('product_list', 600, fn() =>
    Product::all()
);

c) Binary Search PHP:

function binarySearch(array $sortedProducts, string $keyword): int {
    $low  = 0;
    $high = count($sortedProducts) - 1;

    while ($low <= $high) {
        $mid  = intdiv($low + $high, 2);
        $name = strtolower($sortedProducts[$mid]['name']);
        $kw   = strtolower($keyword);

        if ($name === $kw)     return $mid;   // Ketemu
        elseif ($name < $kw)  $low  = $mid + 1;
        else                  $high = $mid - 1;
    }
    return -1; // Tidak ditemukan
}
// Kompleksitas: O(log n) โ€” jauh lebih efisien dari O(n)
Soal 7 J.620100.025.02 Sedang
Debugging & Penanganan Error
Error 500 setelah deploy ke production. Log: SQLSTATE[42P01]: Undefined table: relation "order_details" does not exist a) Apa penyebab error tersebut dan cara mengatasinya? b) Jelaskan langkah debugging sistematis yang Anda lakukan. c) Tulis kode penanganan error yang baik untuk query ke tabel order_details.
โœ… Jawaban / Pembahasan

a) Penyebab Error:

  • Tabel order_details belum ada di database production, padahal sudah ada di local.
  • Kemungkinan: migrasi belum dijalankan (php artisan migrate) di server production, atau file migrasi tidak ikut di-deploy.

b) Langkah Debugging Sistematis:

  1. Baca log error lengkap: storage/logs/laravel.log atau tail -f storage/logs/laravel.log.
  2. Cek apakah tabel ada di DB production: SHOW TABLES; (MySQL) atau \dt (PostgreSQL).
  3. Cek status migrasi: php artisan migrate:status.
  4. Jalankan migrasi yang pending: php artisan migrate --force.
  5. Verifikasi dengan hit endpoint lagi dan pantau log.

c) Error Handling yang Baik:

public function getOrderDetails(int $orderId): JsonResponse {
    try {
        $details = OrderDetail::with('product')
            ->where('id_order', $orderId)
            ->get();

        if ($details->isEmpty()) {
            return response()->json(
                ['message' => 'Tidak ada detail order ditemukan'],
                404
            );
        }

        return response()->json(['data' => $details]);

    } catch (\Illuminate\Database\QueryException $e) {
        // Tangkap error spesifik DB
        Log::error('DB Error getOrderDetails', [
            'order_id' => $orderId,
            'message'  => $e->getMessage(),
        ]);
        return response()->json(
            ['error' => 'Terjadi kesalahan database'],
            500
        );
    }
}
Soal 8 J.620100.006.01 Mudah
User Experience & Antarmuka Web
Anda diminta mendesain halaman daftar produk yang user-friendly untuk toko online. a) Sebutkan minimal 5 prinsip UX yang harus diperhatikan. b) Bagaimana cara membuat tabel data yang responsif menggunakan HTML/CSS saja (tanpa framework)? c) Jelaskan pentingnya feedback visual saat user melakukan action (misal: tambah ke keranjang).
โœ… Jawaban / Pembahasan

a) 5 Prinsip UX:

  1. Visibility of Status โ€” Selalu tampilkan status sistem (loading spinner, "berhasil ditambahkan").
  2. Consistency โ€” Warna, ikon, tombol konsisten di seluruh halaman.
  3. Error Prevention โ€” Validasi form sebelum submit, konfirmasi sebelum hapus.
  4. Efficiency โ€” Fitur search, filter, dan paginasi agar user cepat temukan produk.
  5. Aesthetic & Minimalist โ€” Tampilkan info esensial saja; hindari informasi berlebihan.

b) Tabel Responsif (Pure CSS):

<style>
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
table { width: 100%; border-collapse: collapse; min-width: 500px; }
th, td { padding: 10px 14px; border-bottom: 1px solid #e2e8f0; text-align: left; }
@media (max-width: 600px) {
    /* Stack layout untuk mobile */
    table, thead, tbody, th, td, tr { display: block; }
    td::before { content: attr(data-label); font-weight: 700; }
}
</style>
<div class="table-wrap"> <table> ... </table> </div>

c) Feedback Visual: Setiap aksi user harus mendapat respons visual segera (<100ms). Contoh: tombol berubah warna + teks "Ditambahkan!" saat klik "Tambah ke Keranjang", counter keranjang berubah angka, dan toast notification muncul. Tanpa feedback, user akan klik berkali-kali (double submit) karena tidak tahu aksinya berhasil.

Soal 9 J.620100.021.02 Sulit
Autentikasi & Keamanan Web
Sistem memerlukan fitur autentikasi multi-level: pengunjung, user terdaftar, dan admin. a) Jelaskan perbedaan antara Authentication dan Authorization. b) Bagaimana Laravel mencegah serangan CSRF? Jelaskan mekanismenya. c) Jelaskan cara menyimpan password dengan aman dan tulis kode register + login di Laravel.
โœ… Jawaban / Pembahasan

a) Authentication vs Authorization:

  • Authentication (Autentikasi): Memverifikasi identitas โ€” "Siapa kamu?" Contoh: proses login dengan email + password.
  • Authorization (Otorisasi): Memverifikasi hak akses โ€” "Boleh apa kamu?" Contoh: hanya admin yang boleh hapus produk, meski user sudah login.

b) CSRF Protection Laravel:

  • Laravel generate token unik per session dan menyimpannya di cookie XSRF-TOKEN.
  • Setiap form POST/PUT/DELETE harus menyertakan @csrf (blade) yang menghasilkan hidden input token.
  • Middleware VerifyCsrfToken membandingkan token di request dengan token session. Jika tidak cocok โ†’ 419 error.
{{-- Blade form --}}
<form method="POST" action="/orders">
    @csrf   {{-- <input type="hidden" name="_token" value="..." /> --}}
    ...
</form>

c) Simpan Password + Register/Login:

// Register: hash password sebelum simpan
public function register(Request $request) {
    $request->validate([
        'email'    => 'required|email|unique:users',
        'password' => 'required|min:8|confirmed',
    ]);
    $user = User::create([
        'name'     => $request->name,
        'email'    => $request->email,
        'password' => Hash::make($request->password), // bcrypt
    ]);
    Auth::login($user);
    return redirect('/dashboard');
}

// Login: verifikasi dengan Hash::check
public function login(Request $request) {
    $credentials = $request->only('email', 'password');
    if (Auth::attempt($credentials, $request->boolean('remember'))) {
        $request->session()->regenerate(); // Cegah session fixation
        return redirect()->intended('/dashboard');
    }
    return back()->withErrors(['email' => 'Kredensial tidak valid.']);
}
Soal 10 J.620100.036.02 Sulit
Pengujian Perangkat Lunak
Anda diminta menulis test untuk fitur pembuatan order. a) Jelaskan perbedaan Unit Test, Integration Test, dan Feature Test di Laravel. b) Tulis Feature Test untuk POST /orders: skenario berhasil dan skenario gagal (user belum login). c) Jelaskan apa itu Test-Driven Development (TDD) dan kapan sebaiknya diterapkan.
โœ… Jawaban / Pembahasan

a) Jenis Test di Laravel:

  • Unit Test: Menguji satu fungsi/method terisolasi, tanpa database/HTTP. Contoh: test fungsi calculateTotal().
  • Integration Test: Menguji interaksi antar komponen (misal model + database). Menggunakan DB testing dengan RefreshDatabase.
  • Feature Test: Menguji satu fitur end-to-end dari HTTP request hingga response. Paling umum di Laravel.

b) Feature Test untuk POST /orders:

use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class OrderTest extends TestCase {
    use RefreshDatabase;

    // Skenario 1: Berhasil buat order
    public function test_user_can_create_order(): void {
        $user = User::factory()->create();
        $product = Product::factory()->create(['price' => 50000]);

        $response = $this->actingAs($user)->postJson('/orders', [
            'items' => [['product_id' => $product->id, 'qty' => 2]],
        ]);

        $response->assertStatus(201)
                 ->assertJsonPath('data.status', 'pending');
        $this->assertDatabaseHas('orders', ['customer' => $user->name]);
    }

    // Skenario 2: Gagal - belum login
    public function test_unauthenticated_cannot_create_order(): void {
        $response = $this->postJson('/orders', [
            'items' => [['product_id' => 1, 'qty' => 1]],
        ]);

        $response->assertStatus(401);
    }
}

c) Test-Driven Development (TDD): Pendekatan di mana test ditulis sebelum kode implementasi. Siklus: Red (tulis test yang gagal) โ†’ Green (tulis kode minimal agar test lulus) โ†’ Refactor (perbaiki kode tanpa mengubah fungsionalitas). TDD sebaiknya diterapkan pada fitur kritis (autentikasi, payment, logic bisnis utama), namun bisa terlalu lambat untuk prototyping awal atau UI yang sering berubah.

๐ŸŽค Simulasi Pertanyaan Asesor & Jawaban

โš ๏ธ Persiapan Wawancara Asesor

Daftar pertanyaan yang kemungkinan besar ditanyakan asesor BNSP tentang aplikasi Monity Finance Tracker. Jawab dengan percaya diri dan rujuk ke kode nyata yang ada di proyek kamu. Klik tombol untuk lihat panduan jawaban.

Q1
"Ceritakan aplikasi yang Anda buat. Apa fungsinya dan teknologi apa yang digunakan?"
โœ… Panduan Jawaban

Monity adalah aplikasi personal finance tracker berbasis web yang membantu pengguna mencatat dan memantau keuangan pribadi mereka.

Fitur utama:

  • Pencatatan transaksi (pemasukan & pengeluaran) dengan kategori otomatis
  • Manajemen multi-rekening (bank, e-wallet, kas)
  • Dashboard ringkasan dengan grafik tren 6 bulan
  • AI Insight dari Google Gemini โ€” analisis pola pengeluaran otomatis
  • Autentikasi aman (email/password + Google OAuth)

Teknologi: Next.js 14 (App Router), TypeScript, Drizzle ORM, PostgreSQL (Supabase), NextAuth.js v5, Google Gemini AI, Tailwind CSS, Framer Motion, Zod, React Query.

Q2
"Mengapa Anda memilih Next.js? Apa kelebihannya dibanding framework lain?"
โœ… Panduan Jawaban

Saya memilih Next.js 14 karena beberapa alasan teknis:

  • Full-stack dalam satu project โ€” API route (route.ts) dan halaman UI ada di tempat yang sama, tidak perlu backend terpisah.
  • App Router & Server Components โ€” rendering dilakukan di server untuk performa lebih baik dan SEO-friendly.
  • TypeScript-first โ€” cocok dikombinasikan dengan Drizzle ORM dan Zod untuk type safety end-to-end.
  • Ekosistem React yang matang โ€” banyak library (Framer Motion, React Query, Recharts) yang terintegrasi baik.

Dibanding Laravel (PHP), Next.js lebih cocok untuk aplikasi real-time dengan UI interaktif karena berbasis React.

Q3
"Jelaskan bagaimana Anda menghubungkan aplikasi ke database. Tunjukkan kodenya!"
โœ… Panduan Jawaban

Saya menggunakan Drizzle ORM dengan postgres.js untuk koneksi ke PostgreSQL di Supabase. Koneksi dikonfigurasi di src/lib/db/index.ts:

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

// Connection string dari environment variable
const sql = postgres(process.env.DATABASE_URL!, {
    prepare: false  // wajib untuk Supabase Transaction Pool mode
});

// Instance tunggal db, dipakai di semua API route
export const db = drizzle(sql, { schema });

Kenapa prepare: false? Karena Supabase menggunakan Transaction Pooler yang tidak mendukung prepared statements.

Schema tabel dibuat di src/lib/db/schema.ts menggunakan TypeScript, lalu di-push ke DB dengan npx drizzle-kit push.

Q4
"Bagaimana sistem autentikasi di aplikasi Anda bekerja?"
โœ… Panduan Jawaban

Saya menggunakan NextAuth.js v5 yang mendukung dua metode login:

  1. Email & Password โ€” Password di-hash dengan bcryptjs sebelum disimpan ke DB. Saat login, hash dibandingkan menggunakan bcrypt.compare().
  2. Google OAuth โ€” User bisa login dengan akun Google. NextAuth menangani seluruh alur OAuth secara otomatis.

Session disimpan di tabel session (database strategy). Setiap API route di-protect dengan pengecekan session:

const session = await auth();
if (!session?.user?.id) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

Untuk proteksi halaman, saya menggunakan middleware di middleware.ts yang mengecek session sebelum request sampai ke halaman dashboard.

Q5
"Apa itu OOP? Apakah aplikasi Anda menggunakannya? Berikan contoh!"
โœ… Panduan Jawaban

OOP (Object-Oriented Programming) adalah paradigma yang mengorganisir kode dalam bentuk objek dengan properti dan method. 4 pilarnya: Encapsulation, Inheritance, Polymorphism, Abstraction.

Di Monity (TypeScript), OOP diwujudkan dengan:

  • Interface โ€” blueprint/kontrak tipe data, misalnya FinancialSummary di src/lib/ai/gemini.ts.
  • Class โ€” new GoogleGenerativeAI(apiKey) untuk instantiasi objek Gemini AI.
  • Encapsulation via Zod โ€” validasi data tersembunyi di dalam schema, tidak bisa bypass dari luar.
  • Type Inference โ€” typeof transactions.$inferSelect menghasilkan tipe otomatis dari schema database.
// Interface = kontrak data (seperti abstract class PHP)
export interface FinancialSummary {
    totalIncome: number;
    totalExpense: number;
    topExpenseCategories: { category: string; amount: number }[];
}

// Class instantiation
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash-lite" });
Q6
"Bagaimana Anda melakukan validasi input dari pengguna?"
โœ… Panduan Jawaban

Saya melakukan validasi di dua lapisan:

  1. Frontend (React Hook Form) โ€” validasi real-time saat user mengisi form, sebelum data dikirim ke server.
  2. Backend (Zod) โ€” validasi di API route sebelum data diproses atau disimpan ke database. Ini lapisan keamanan utama.
// src/lib/validations.ts โ€” Zod schema
export const createTransactionSchema = z.object({
    accountId: z.string().min(1, "Akun wajib dipilih"),
    type:      z.enum(["income", "expense"]),
    amount:    z.coerce.number().positive("Nominal harus lebih dari 0"),
    transactionDate: z.coerce.date(),
    categoryId:  z.string().optional(),
    description: z.string().optional(),
});

// Di API route โ€” validasi sebelum insert
const parsed = createTransactionSchema.safeParse(body);
if (!parsed.success) {
    return NextResponse.json({ error: "Validation failed" }, { status: 400 });
}

Dengan Zod, pesan error otomatis muncul dan tipe data sudah tervalidasi. Tidak ada cara bypass validasi dari client.

Q7
"Jelaskan bagaimana Anda menangani error di aplikasi ini!"
โœ… Panduan Jawaban

Saya menerapkan error handling berlapis:

  • try/catch di setiap API route โ€” semua operasi async dibungkus try/catch.
  • HTTP Status Code yang tepat โ€” 400 (validasi gagal), 401 (belum login), 404 (data tidak ada), 409 (konflik data), 500 (server error).
  • console.error untuk logging โ€” error dicatat di server log untuk debugging.
  • Frontend toast notification โ€” user mendapat pesan yang informatif sesuai response API.
try {
    const parsed = createTransactionSchema.safeParse(body);
    if (!parsed.success)
        return NextResponse.json({ error: "Validation failed" }, { status: 400 });

    // ... proses bisnis

    return NextResponse.json({ transaction: result }, { status: 201 });

} catch (error) {
    console.error("Transaction create error:", error); // Log ke server
    return NextResponse.json({ error: "Terjadi kesalahan server" }, { status: 500 });
}
Q8
"Apa itu migrasi database? Bagaimana Anda mengelola perubahan schema?"
โœ… Panduan Jawaban

Migrasi database adalah proses mengubah struktur database (tambah tabel, kolom, relasi) secara terversi dan terkontrol, bukan manual lewat GUI.

Di Monity, saya menggunakan Drizzle Kit. Alurnya:

  1. Ubah schema TypeScript di src/lib/db/schema.ts
  2. Jalankan npx drizzle-kit generate โ†’ generate file SQL migrasi di folder drizzle/
  3. Jalankan npx drizzle-kit push โ†’ apply langsung ke Supabase PostgreSQL

Keuntungannya: schema tersimpan di kode (version control), perubahan bisa di-rollback, dan konsisten di semua environment (local, production).

-- File drizzle/0000_smiling_blue_blade.sql (di-generate otomatis)
CREATE TABLE "transaction" (
    "id"           TEXT PRIMARY KEY NOT NULL,
    "user_id"      TEXT NOT NULL REFERENCES "user"(id) ON DELETE CASCADE,
    "account_id"   TEXT NOT NULL REFERENCES "finance_account"(id) ON DELETE CASCADE,
    "amount"       NUMERIC(15, 2) NOT NULL,
    "created_at"   TIMESTAMP DEFAULT now() NOT NULL
);
Q9
"Bagaimana cara Anda mengambil data transaksi dengan filter tanggal dan sorting?"
โœ… Panduan Jawaban

Saya menggunakan Drizzle ORM query builder yang menghasilkan SQL asli secara type-safe. Di src/app/api/transactions/route.ts:

// TypeScript / Drizzle ORM
const result = await db.query.transactions.findMany({
    where: and(
        eq(transactions.userId, session.user.id),
        gte(transactions.transactionDate, startDate),  // >= tanggal awal
        lte(transactions.transactionDate, endDate),    // <= tanggal akhir
        type ? eq(transactions.type, type) : undefined // filter income/expense
    ),
    with: {
        account:  true,  // LEFT JOIN ke tabel finance_account
        category: true,  // LEFT JOIN ke tabel category
    },
    orderBy: [desc(transactions.transactionDate)],  // sort descending
    limit:  50,   // maksimal 50 per halaman
    offset: page * 50, // paginasi
});

SQL yang di-generate setara dengan:

SELECT t.*, a.name as account_name, c.name as category_name
FROM "transaction" t
LEFT JOIN "finance_account" a ON t.account_id = a.id
LEFT JOIN "category" c ON t.category_id = c.id
WHERE t.user_id = $1
  AND t.transaction_date >= $2
  AND t.transaction_date <= $3
ORDER BY t.transaction_date DESC
LIMIT 50 OFFSET 0;
Q10
"Bagaimana Artificial Intelligence digunakan di aplikasi ini?"
โœ… Panduan Jawaban

Saya mengintegrasikan Google Gemini AI (model gemini-2.5-flash-lite) untuk fitur Financial Insight. Ada 3 jenis insight:

  • monthly_summary โ€” ringkasan keuangan bulanan (total income, expense, saldo).
  • spending_alert โ€” peringatan jika pengeluaran bulan ini naik signifikan dibanding bulan lalu.
  • saving_tip โ€” tips menabung berdasarkan pola pengeluaran kategori terbesar.

Cara kerjanya: data transaksi dikumpulkan โ†’ diformat menjadi summary โ†’ dikirim ke Gemini sebagai prompt โ†’ Gemini menghasilkan teks insight dalam Bahasa Indonesia โ†’ insight disimpan di tabel insight dan ditampilkan di dashboard.

// src/lib/ai/gemini.ts
const model = genAI.getGenerativeModel({ model: "gemini-2.5-flash-lite" });
const prompt = `Kamu adalah asisten keuangan pribadi...
Data keuangan bulan ini: ${JSON.stringify(summary)}
Berikan insight singkat dalam Bahasa Indonesia...`;
const result = await model.generateContent(prompt);
const text   = result.response.text();
Q11
"Bagaimana struktur folder proyek Anda dan mengapa diorganisir seperti itu?"
โœ… Panduan Jawaban

Proyek menggunakan struktur Next.js App Router yang modular โ€” setiap folder punya tanggung jawab yang jelas (Separation of Concerns):

src/
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ (auth)/          โ† Halaman Login & Register
โ”‚   โ”œโ”€โ”€ (dashboard)/     โ† Dashboard, Transaksi, Akun, Insight
โ”‚   โ””โ”€โ”€ api/             โ† REST API handlers (route.ts)
โ”‚       โ”œโ”€โ”€ transactions/
โ”‚       โ”œโ”€โ”€ accounts/
โ”‚       โ”œโ”€โ”€ insights/
โ”‚       โ””โ”€โ”€ auth/
โ”œโ”€โ”€ components/
โ”‚   โ”œโ”€โ”€ layout/          โ† Sidebar, Navbar
โ”‚   โ”œโ”€โ”€ dashboard/       โ† Widget summary, chart
โ”‚   โ”œโ”€โ”€ transactions/    โ† Form, list transaksi
โ”‚   โ””โ”€โ”€ ui/              โ† Button, Modal, Dialog (reusable)
โ””โ”€โ”€ lib/
    โ”œโ”€โ”€ db/              โ† Schema Drizzle + koneksi
    โ”œโ”€โ”€ ai/              โ† Gemini integration
    โ”œโ”€โ”€ auth/            โ† NextAuth config
    โ”œโ”€โ”€ hooks/           โ† Custom React hooks
    โ”œโ”€โ”€ validations.ts   โ† Semua Zod schema
    โ””โ”€โ”€ utils.ts         โ† Helper functions

Keuntungan: mudah ditemukan, mudah di-maintain, dan setiap bagian bisa diuji secara terpisah.

Q12
"Bagaimana Anda memastikan keamanan data pengguna di aplikasi ini?"
โœ… Panduan Jawaban

Keamanan diterapkan di beberapa lapisan:

  1. Password hashing โ€” bcryptjs dengan salt rounds 10. Password tidak pernah disimpan plain text.
  2. Session-based auth โ€” NextAuth.js mengelola session di server; token tidak terekspos ke client.
  3. Row-level security โ€” setiap query selalu memfilter by user_id dari session yang terverifikasi, bukan dari input client.
  4. Zod validation โ€” mencegah SQL injection dan data malformasi dari input user.
  5. Environment variables โ€” semua secret (DB URL, API key) ada di .env, tidak di-hardcode dan tidak di-commit ke git.
  6. HTTPS (Supabase) โ€” koneksi database terenkripsi TLS/SSL.
// Setiap query selalu scope ke user yang login
const transactions = await db.query.transactions.findMany({
    where: eq(transactions.userId, session.user.id), // โ† WAJIB ada!
});
Q13
"Bagaimana cara Anda menguji (testing) aplikasi ini?"
โœ… Panduan Jawaban

Saya melakukan pengujian melalui beberapa pendekatan:

  1. Zod safeParse() sebagai unit test fungsional โ€” setiap kali schema diubah, validasi langsung diuji dengan input valid dan invalid.
  2. Manual API testing โ€” setiap endpoint diuji dengan berbagai skenario: input valid, input invalid, tanpa login, data tidak ditemukan.
  3. Browser testing โ€” UI diuji di Chrome, Firefox, dan mobile untuk memastikan responsivitas.

Contoh skenario yang sudah diuji:

  • POST /api/transactions dengan amount negatif โ†’ 400 Bad Request โœ…
  • GET /api/transactions tanpa session โ†’ 401 Unauthorized โœ…
  • POST /api/auth/register dengan email yang sama โ†’ 409 Conflict โœ…
  • DELETE /api/accounts/:id โ†’ balance rekening lain tidak terpengaruh โœ…
Q14
"Apa kesulitan terbesar saat membangun aplikasi ini dan bagaimana Anda mengatasinya?"
โœ… Panduan Jawaban

Ada beberapa tantangan teknis yang saya hadapi:

  1. Sinkronisasi saldo rekening โ€” Saat transaksi dibuat/diupdate/dihapus, saldo rekening harus ikut berubah secara akurat.
    Solusi: Menggunakan SQL expression balance + balanceChange langsung di query UPDATE untuk operasi atomic, bukan read-update terpisah.
  2. Supabase Connection Pool โ€” Awalnya error karena prepared statement tidak didukung.
    Solusi: Menambahkan prepare: false di konfigurasi postgres.js.
  3. Prompt engineering Gemini AI โ€” Respons AI tidak konsisten formatnya.
    Solusi: Membuat prompt yang sangat spesifik dengan instruksi format output yang jelas dan membatasi panjang output.
Q15
"Jika ada waktu lebih, fitur apa yang ingin Anda tambahkan?"
โœ… Panduan Jawaban

Ada beberapa fitur yang ingin saya kembangkan:

  • Budget Planning โ€” user bisa set anggaran per kategori per bulan, dengan alert ketika mendekati batas.
  • Export laporan โ€” unduh laporan transaksi dalam format PDF atau Excel untuk keperluan pembukuan.
  • Transfer antar rekening โ€” fitur transfer yang otomatis mengurangi saldo rekening asal dan menambah saldo rekening tujuan.
  • Recurring transaction โ€” transaksi berulang otomatis (gaji bulanan, tagihan rutin).
  • Mobile app โ€” versi PWA (Progressive Web App) agar bisa diinstall di smartphone.

Ini menunjukkan bahwa saya memahami kebutuhan pengguna dan punya roadmap pengembangan yang jelas.

Monity Finance Tracker ยท Dibuat untuk BNSP Skema J.620100 Web Developer ยท Februari 2026
17 / 17 KUK Terpenuhi