Perencanaan proyek

Merancang skema, fitur, dan permukaan API aplikasi catatan pribadi.

Daftar isi

Perencanaan proyek menetapkan tiga keputusan sebelum kode implementasi ditulis, yaitu skema database, daftar fitur, dan permukaan API (API surface) sebagai kumpulan endpoint yang dapat dipanggil klien. Topik ini menyusun ketiganya untuk aplikasi catatan pribadi yang menjadi contoh sepanjang jalur, mulai dari pemetaan fitur menjadi kebutuhan data, penetapan tabel beserta relasinya, hingga tabel kontrak endpoint beserta status jawabannya. Hasil akhirnya berupa rancangan tertulis yang dapat dibandingkan dengan skema yang benar-benar berjalan, dan pemeriksaan kesesuaiannya dijalankan langsung pada proyek uji di akhir topik.

Fitur inti dan batas aplikasi#

Aplikasi catatan pribadi menyimpan catatan yang dikelompokkan ke dalam kategori, dan keputusan paling menentukan justru terletak pada batasnya. Aplikasi ini bekerja dalam mode pribadi, yaitu seluruh catatan dikelola oleh satu pemilik data, sehingga tabel catatan tidak memuat kolom pemilik. Fitur inti yang dirancang terdiri atas tiga hal, yaitu menyimpan catatan baru melalui API, membaca daftar maupun detail catatan, dan mengelompokkan catatan melalui kategori bernama. Fitur lanjutan seperti pengingat terjadwal atau ekspor arsip ditempatkan di atas fondasi tersebut, sehingga penambahannya tidak mengubah kontrak inti yang sudah disepakati.

Setiap fitur menuntut data tertentu, dan pemetaannya langsung menjadi dasar skema. Tabel berikut merangkum pemetaan tersebut.

FiturData yang dibutuhkan
Mengelompokkan catatanTabel kategori berisi nama kategori yang tetap
Menyimpan catatanTabel catatan berisi judul, isi, dan penghubung ke kategori
Membaca daftar dan detailPenanda waktu pembuatan dan perubahan sebagai dasar pengurutan

Tiga fitur ini membutuhkan dua tabel, yaitu categories dan notes, dengan relasi satu ke banyak dari kategori ke catatan. Jumlah tabel yang sedikit bukan berarti keputusannya sedikit, karena setiap kolom membawa keputusan tersendiri yang sulit diubah setelah data terisi.

Skema data: tabel, kolom, dan relasi#

Tabel categories menyimpan satu kolom isi berupa nama, sedangkan tabel notes menyimpan judul, isi, dan category_id sebagai penghubung. Dua keputusan pada kolom category_id menentukan perilaku aplikasi untuk waktu yang lama. Keputusan pertama adalah kolom tersebut boleh kosong, karena catatan pribadi kerap ditulis tanpa kategori, dan larangan bernilai kosong hanya memaksa pengguna membuat kategori tidak bermakna. Keputusan kedua adalah kolom tersebut dibatasi sebagai kunci asing (foreign key) yang menunjuk ke categories, sehingga database menolak catatan yang menunjuk kategori yang tidak ada.

Kedua keputusan tersebut dituliskan pada migration, yaitu berkas PHP yang membentuk dan mengubah struktur tabel. Potongan berikut memperlihatkan bentuk pembentukan tabel notes sebagaimana dirancang.

php Rancangan tabel notes dalam migration
// tidak-dijalankan
Schema::create('notes', function (Blueprint $table) {
    $table->id();
    $table->string('judul');
    $table->text('isi');
    $table->foreignId('category_id')->nullable()->constrained();
    $table->timestamps();
});

Metode nullable() pada baris category_id mengizinkan catatan tanpa kategori, sedangkan constrained() membuat kolomnya menjadi kunci asing yang mengikuti konvensi penamaan Laravel, yaitu menunjuk tabel categories pada kolom id. Metode timestamps() menambahkan created_at dan updated_at, dan daftar catatan menggunakannya sebagai dasar pengurutan. Skema dengan bentuk seperti ini dapat diubah kemudian, tetapi setiap perubahan setelah data terisi menuntut migration tambahan, sehingga keputusan awal sepadan dengan usahanya.

Permukaan API: endpoint dan kontrak respons#

Permukaan API adalah daftar endpoint yang boleh dipanggil klien beserta bentuk jawabannya, dan daftar ini menjadi kontrak yang mengikat implementasi. Seluruh endpoint ditempatkan di bawah awalan /api dan diberi nama mengikuti sumber daya yang dilayaninya, yaitu catatan, sehingga klien tidak perlu menebak bentuk alamatnya. Bentuk jawaban yang berhasil menggunakan pembungkus data yang ditetapkan satu kali pada kelas API resource, sehingga seluruh endpoint berbagi struktur JSON yang sama. Tabel berikut memuat kontrak lengkapnya.

MethodURITujuanStatus jawaban
GET/api/catatanDaftar catatan beserta kategorinya200
GET/api/catatan/{id}Detail satu catatan200, atau 404 apabila tidak ditemukan
POST/api/catatanMenyimpan catatan baru201, 422 apabila validasi gagal, 401 tanpa token

Rute tulis dibedakan dari rute baca melalui perlindungannya. POST /api/catatan dilindungi middleware auth:sanctum, sehingga permintaan tanpa token dijawab dengan status 401 sebelum mencapai logika aplikasi, sedangkan kesalahan isi permintaan dijawab dengan status 422 beserta daftar error per kolom. Ketiga status gagal pada tabel di atas bukan detail pelengkap, melainkan bagian dari kontrak yang kelak ditegakkan oleh pengujian. Peta permukaan API tersebut digambarkan sebagai berikut.

Klien APIJSONtokenGET /api/catatandaftar catatan beserta kategorinya200GET /api/catatan/{id}detail satu catatan200 atau 404POST /api/catatanmenyimpan catatan baru, dilindungi auth:sanctum201, 422, atau 401
Permukaan API aplikasi catatan beserta status jawabannya

Memeriksa kesesuaian rancangan dengan skema berjalan#

Rancangan bernilai penuh hanya apabila dapat dibandingkan dengan realisasinya. Blok berikut menyiapkan tiga kategori bawaan secara deterministik pada proyek uji, lalu membaca kolom tabel yang benar-benar berjalan sebagai bahan pembanding pertama.

bash
# menyiapkan kategori bawaan lalu membaca kolom skema yang berjalan
cd ~/aplikasi-catatan
php artisan tinker --execute='
use App\Models\Category;
use Illuminate\Support\Facades\Schema;
foreach (["Kerja", "Belajar", "Pribadi"] as $nama) {
    Category::firstOrCreate(["nama" => $nama]);
}
echo "kategori: ".Category::whereIn("nama", ["Kerja", "Belajar", "Pribadi"])->orderBy("nama")->pluck("nama")->implode(", ").PHP_EOL;
echo "kolom categories: ".implode(", ", Schema::getColumnListing("categories")).PHP_EOL;
echo "kolom notes: ".implode(", ", Schema::getColumnListing("notes")).PHP_EOL;
'
text Keluaran
kategori: Belajar, Kerja, Pribadi
kolom categories: id, nama, created_at, updated_at
kolom notes: id, judul, isi, category_id, created_at, updated_at

Daftar kolom yang terbaca sesuai dengan tabel rancangan pada bagian sebelumnya. Pemeriksaan berikutnya lebih ketat, yaitu membandingkan daftar kolom yang diharapkan dengan daftar aktual secara utuh, memeriksa sifat boleh kosong pada category_id, memastikan kunci asingnya terhubung ke categories, dan menghitung kategori bawaan. Seluruh pemeriksaan itu dirangkum dalam satu skrip PHP yang dapat dijalankan ulang kapan pun.

php periksa-rancangan.php
<?php

use App\Models\Category;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

$root = getcwd();
if (! is_file($root.'/bootstrap/app.php')) {
    $root = getenv('HOME').'/aplikasi-catatan';
}

require $root.'/vendor/autoload.php';

$app = require $root.'/bootstrap/app.php';
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();

// kategori bawaan disiapkan secara deterministik
foreach (['Kerja', 'Belajar', 'Pribadi'] as $nama) {
    Category::firstOrCreate(['nama' => $nama]);
}

// rancangan kolom yang disepakati saat perencanaan
$rancangan = [
    'categories' => ['created_at', 'id', 'nama', 'updated_at'],
    'notes' => ['category_id', 'created_at', 'id', 'isi', 'judul', 'updated_at'],
];

foreach ($rancangan as $tabel => $kolom) {
    $aktual = Schema::getColumnListing($tabel);
    sort($aktual);
    $cocok = $kolom === $aktual
        ? 'sesuai rancangan'
        : 'berbeda: '.implode(', ', $aktual);
    echo 'tabel '.$tabel.': '.$cocok.PHP_EOL;
}

$kolom = collect(Schema::getColumns('notes'))->firstWhere('name', 'category_id');
echo 'category_id nullable: '.var_export($kolom['nullable'], true).PHP_EOL;

$kunci = DB::select('PRAGMA foreign_key_list(notes)');
$terhubung = collect($kunci)->contains(
    fn ($k) => $k->from === 'category_id' && $k->table === 'categories' && $k->to === 'id'
);
echo 'foreign key category_id -> categories(id): '.($terhubung ? 'ada' : 'tidak ada').PHP_EOL;

$jumlah = Category::whereIn('nama', ['Kerja', 'Belajar', 'Pribadi'])->count();
$nama = Category::whereIn('nama', ['Kerja', 'Belajar', 'Pribadi'])->orderBy('nama')->pluck('nama')->implode(', ');
echo 'kategori bawaan: '.$jumlah.' dari 3 ('.$nama.')'.PHP_EOL;
text Keluaran
tabel categories: sesuai rancangan
tabel notes: sesuai rancangan
category_id nullable: true
foreign key category_id -> categories(id): ada
kategori bawaan: 3 dari 3 (Belajar, Kerja, Pribadi)

Kelima baris keluaran menegaskan bahwa skema yang berjalan sama dengan rancangan yang ditulis, mulai dari daftar kolom kedua tabel, sifat boleh kosong pada category_id, kunci asingnya, hingga tiga kategori bawaan. Pembacaan kolom melalui tinker cocok untuk pemeriksaan sekali jalan, sedangkan skrip menyimpan perbandingan yang dapat dijalankan ulang tanpa menuliskan perintahnya kembali. Skrip semacam ini sekaligus menjadi pemeriksa cepat ketika skema berkembang, karena setiap migration baru cukup diikuti penambahan baris rancangan yang sesuai.

Kesalahan umum#

Kesalahan pertama adalah menunda keputusan boleh kosong sampai implementasi berjalan. Kolom category_id yang dideklarasikan tanpa nullable() memaksa setiap catatan memiliki kategori, dan perbaikannya setelah data terisi menuntut migration perubahan kolom alih-alih penyuntingan satu kata. Keputusan nullable() dan constrained() sebaiknya dipatok pada migration pertama seperti pada potongan rancangan di atas.

Kesalahan kedua adalah menyusun permukaan API tanpa kontrak respons yang konsisten. Endpoint yang satu membungkus jawabannya dengan data sedangkan yang lain tidak membuat klien menulis penanganan ganda untuk struktur yang seharusnya sama. Perbaikannya adalah menetapkan transformasi JSON pada satu kelas API resource dan menuliskan bentuk jawabannya pada tabel kontrak sebelum endpoint ditulis.

Kesalahan ketiga adalah menyimpan rancangan hanya di kepala. Skema yang tidak tertulis berubah mengikuti arus implementasi, dan penyimpangan baru ketahuan ketika fitur frontend sudah dibangun di atas bentuk jawaban yang keliru. Perbaikannya adalah menuliskan tabel fitur, kolom, dan endpoint seperti pada topik ini, lalu menjalankan skrip pemeriksaan secara berkala agar realisasi tidak berjalan sendiri.

Ringkasan#

  • Perencanaan menetapkan tiga keputusan sebelum kode ditulis, yaitu daftar fitur beserta kebutuhan datanya, skema dua tabel categories dan notes, serta permukaan API dengan status jawabannya.
  • Kolom category_id dirancang boleh kosong sekaligus terhubung sebagai kunci asing ke categories, dan kedua sifat itu dituliskan pada migration pertama.
  • Kontrak respons menetapkan pembungkus data yang sama untuk seluruh endpoint, dengan 401 untuk permintaan tanpa token, 422 untuk isi yang gagal validasi, dan 404 untuk data yang tidak ditemukan.
  • Skrip pemeriksaan membandingkan rancangan dengan skema berjalan sehingga penyimpangan terdeteksi sebelum implementasi dimulai.

Referensi#