Setelah menyelesaikan website blog berbasis HTML, kita berganti medium: aplikasi yang kita bangun sekarang tidak lagi menghasilkan halaman web untuk manusia, melainkan data JSON untuk dikonsumsi program lain. Pada Bagian 1 ini kita menyiapkan seluruh fondasi proyek REST API manajemen tugas — struktur direktori, berkas composer.json, front controller di public/index.php, dan endpoint pertama GET /api/kesehatan. Fondasi yang rapi akan membayar dirinya sendiri: enam bagian berikutnya cukup menambahkan kelas baru tanpa membongkar yang sudah ada.
Arsitektur Proyek #
Proyek ini sengaja dibangun tanpa framework supaya kita memahami apa yang sebenarnya dilakukan framework di balik layar. Seluruh logika hidup di kelas-kelas namespace App\ di dalam direktori src/, sementara satu-satunya berkas yang bisa diakses web adalah front controller:
rest-api-manajemen-tugas/
├── composer.json konfigurasi package + autoloading PSR-4
├── public/
│ └── index.php front controller — satu pintu masuk semua request
├── src/ kelas-kelas aplikasi (namespace App\)
│ ├── Router.php mulai muncul di Bagian 2
│ ├── Request.php mulai muncul di Bagian 2
│ ├── Response.php mulai muncul di Bagian 2
│ ├── Database.php mulai muncul di Bagian 3
│ └── TaskRepository.php mulai muncul di Bagian 3
└── database/
└── schema.sql skema tabel tasks (Bagian 3)Alur satu request dari awal sampai akhir kelak menjadi pipeline pendek:
Client mengirim HTTP request ke public/index.php, autoloader memuat kelas yang dibutuhkan, router mencocokkan path dengan handler, handler membangun objek Response berisi JSON, lalu response dikirim balik ke client. Diagram di atas adalah gambaran arah akhir proyek; pada bagian ini kita baru membangun dua blok pertama setelah client — front controller beserta autoloader-nya — dan cara mengirim response JSON.
Autoloading PSR-4 dengan composer.json #
Berkas composer.json mendeklarasikan identitas package, dependensi, dan aturan autoloading. Bagian terpenting adalah pemetaan PSR-4: namespace App\ dipetakan ke direktori src/, sehingga kelas App\Router otomatis dicari di src/Router.php tanpa satu pun require manual.
{
"name": "budi/rest-api-manajemen-tugas",
"description": "REST API manajemen tugas dengan PHP murni, tanpa framework.",
"type": "project",
"license": "MIT",
"require": {
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^12.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"authors": [
{
"name": "Budi",
"email": "[email protected]"
}
],
"config": {
"sort-packages": true
}
}Dependensi runtime proyek ini hanya PHP 8.3 ke atas karena kita tidak memakai library eksternal. PHPUnit diletakkan di require-dev karena hanya dibutuhkan saat menjalankan test, bukan saat melayani request; test suite lengkap akan kita tulis pada Bagian 6.
Front Controller dengan Guard CLI #
Front controller adalah satu-satunya berkas PHP yang diekspos ke web. Berikut isi lengkap public/index.php pada tahap ini:
<?php
declare(strict_types=1);
/**
* Front controller REST API Manajemen Tugas.
*
* Seluruh request HTTP masuk melalui berkas ini. Pada Bagian 1 kita baru
* melayani satu endpoint: GET /api/kesehatan. Routing akan dipindahkan ke
* kelas Router terpisah pada Bagian 2.
*/
// ---------------------------------------------------------------------------
// 1. Autoloading PSR-4
// ---------------------------------------------------------------------------
// Prioritas pertama adalah vendor/autoload.php hasil `composer install`.
// Bila direktori vendor belum ada, kita memasang autoloader kecil sendiri
// dengan aturan yang sama: kelas App\Foo\Bar dipetakan ke src/Foo/Bar.php.
$projectRoot = dirname(__DIR__);
if (is_file($projectRoot . '/vendor/autoload.php')) {
require $projectRoot . '/vendor/autoload.php';
} else {
spl_autoload_register(static function (string $class): void {
$prefix = 'App\\';
if (!str_starts_with($class, $prefix)) {
return;
}
$relative = str_replace('\\', '/', substr($class, strlen($prefix)));
$file = dirname(__DIR__) . '/src/' . $relative . '.php';
if (is_file($file)) {
require $file;
}
});
}
// ---------------------------------------------------------------------------
// 2. Handler dan pengirim response JSON
// ---------------------------------------------------------------------------
/**
* Handler endpoint GET /api/kesehatan.
*
* @return array<string, string> payload siap di-encode menjadi JSON.
*/
function handleKesehatan(): array
{
return ['status' => 'ok'];
}
/**
* Kirim payload array sebagai response JSON ke client.
*
* @param array<string, mixed> $payload data yang akan di-encode.
*/
function kirimJson(array $payload, int $status = 200): void
{
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), "\n";
}
// ---------------------------------------------------------------------------
// 3. Guard CLI
// ---------------------------------------------------------------------------
// `php public/index.php` dari terminal tidak membawa request HTTP, sehingga
// superglobal $_SERVER['REQUEST_METHOD'] tidak terisi. Alih-alih crash, kita
// cetak petunjuk ramah lalu keluar dengan exit code 0. Pemeriksaan ganda
// (SAPI + REQUEST_METHOD) membuat front controller tetap bisa diuji dari CLI
// dengan superglobal tiruan — teknik yang akan dipakai lagi pada Bagian 6.
if (PHP_SAPI === 'cli' && !isset($_SERVER['REQUEST_METHOD'])) {
echo "REST API Manajemen Tugas\n";
echo "PHP murni, tanpa framework\n";
echo "\n";
echo "Berkas ini adalah front controller, bukan skrip CLI.\n";
echo "Jalankan API lewat web server bawaan PHP:\n";
echo " php -S 127.0.0.1:8090 -t public\n";
echo "\n";
echo "Endpoint yang tersedia pada Bagian 1:\n";
echo " GET /api/kesehatan memeriksa status layanan\n";
exit(0);
}
// ---------------------------------------------------------------------------
// 4. Dispatch sederhana (digantikan kelas Router pada Bagian 2)
// ---------------------------------------------------------------------------
$method = $_SERVER['REQUEST_METHOD'];
$path = '/' . trim((string) parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH), '/');
if ($method === 'GET' && $path === '/api/kesehatan') {
kirimJson(handleKesehatan());
return;
}
kirimJson(
[
'error' => [
'code' => 'NOT_FOUND',
'message' => "Endpoint $method $path tidak ditemukan.",
],
],
404,
);Mari kita bedah per blok, karena berkas ini akan ditiru oleh seluruh bagian berikutnya:
- Blok 1 (autoloading):
dirname(__DIR__)menghasilkan path root proyek karena berkas ini berada satu tingkat di dalampublic/. Bilavendor/autoload.phpada, Composer yang mengurus semuanya; bila tidak ada,spl_autoload_register()memasang closure yang memotong prefixApp\, mengubah sisa nama kelas menjadi path, lalurequireberkasnya bila memang ada. - Blok 2 (handler):
handleKesehatan()mengembalikan array murni — tidak menyentuhechomaupunheader(). Pemisahan ini membuat handler mudah dipanggil langsung dari skrip pengujian. FungsikirimJson()adalah satu-satunya tempat yang berurusan dengan protokol HTTP. - Blok 3 (guard CLI): konstanta
PHP_SAPIbernilai"cli"ketika berkas dijalankan lewat terminal. Karena terminal tidak mengirim request HTTP,$_SERVER['REQUEST_METHOD']juga tidak terisi; dua syarat itu bersama-sama menjadi tanda “ini bukan request web”, sehingga kita cetak petunjuk ramah dan keluar denganexit(0)— bukan pesan error yang menakutkan. - Blok 4 (dispatch):
parse_url(..., PHP_URL_PATH)mengambil path bersih dari URI lengkap (membuang query string), lalutrim()menormalkan garis miring berlebih. Cocoknya method dan path dievaluasi dengan satu kondisiif; semua yang lain jatuh ke response 404 berbentuk JSON.
Guard CLI bisa kita lihat langsung hasilnya tanpa web server:
<?php
// Menjalankan front controller dari terminal tidak memicu error:
// guard CLI mencetak petunjuk penggunaan lalu keluar dengan exit code 0.
require __DIR__ . '/public/index.php';Output yang Dihasilkan:
REST API Manajemen Tugas
PHP murni, tanpa framework
Berkas ini adalah front controller, bukan skrip CLI.
Jalankan API lewat web server bawaan PHP:
php -S 127.0.0.1:8090 -t public
Endpoint yang tersedia pada Bagian 1:
GET /api/kesehatan memeriksa status layananEndpoint Kesehatan dan Response JSON #
Handler kesehatan bisa dipanggil langsung tanpa web server dengan trik kecil: isi $_SERVER dengan nilai tiruan sebelum memuat front controller, lalu tangkap output-nya menggunakan output buffering (ob_start() dan ob_get_clean()). Teknik yang sama akan kita pakai untuk menguji API secara menyeluruh pada Bagian 6.
<?php
// Menyamar sebagai request web GET /api/kesehatan.
$_SERVER['REQUEST_METHOD'] = 'GET';
$_SERVER['REQUEST_URI'] = '/api/kesehatan';
ob_start();
require __DIR__ . '/public/index.php';
$body = ob_get_clean();
echo "Body response:\n$body";Output yang Dihasilkan:
Body response:
{"status":"ok"}Path yang tidak dikenal jatuh ke cabang 404 dan tetap menerima JSON — konsisten, bukan halaman error HTML bawaan server. Perhatikan bahwa nilai $_SERVER['REQUEST_METHOD'] yang kita injeksi kali ini adalah DELETE:
<?php
// Method dan path yang tidak terdaftar -> 404 dalam bentuk JSON.
$_SERVER['REQUEST_METHOD'] = 'DELETE';
$_SERVER['REQUEST_URI'] = '/api/tugas/1';
ob_start();
require __DIR__ . '/public/index.php';
$body = ob_get_clean();
echo "Body response:\n$body";Output yang Dihasilkan:
Body response:
{"error":{"code":"NOT_FOUND","message":"Endpoint DELETE /api/tugas/1 tidak ditemukan."}}Satu detail kecil yang berdampak besar pada kenyamanan klien API: flag json_encode(). Secara default PHP meng-escape garis miring (/ menjadi \/) meskipun JSON tidak menuntutnya. Bandingkan kedua baris hasil berikut:
<?php
$data = ['judul' => 'Rilis v1.0 "Manajemen Tugas"', 'url' => 'https://example.com/tugas'];
echo 'Default : ', json_encode($data), "\n";
echo 'Rapi + aman : ', json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), "\n";Output yang Dihasilkan:
Default : {"judul":"Rilis v1.0 \"Manajemen Tugas\"","url":"https:\/\/example.com\/tugas"}
Rapi + aman : {"judul":"Rilis v1.0 \"Manajemen Tugas\"","url":"https://example.com/tugas"}Flag JSON_UNESCAPED_SLASHES menjaga URL tetap terbaca manusia, dan JSON_UNESCAPED_UNICODE menjaga teks non-ASCII (mis. emoji atau teks beraksen) tetap tampil apa adanya. Karena itu fungsi kirimJson() selalu memakai kombinasi kedua flag tersebut.
Menguji API dengan Web Server Bawaan #
Sekarang giliran pengujian sungguhan. Web server bawaan PHP dijalankan dari direktori proyek dengan opsi -t public yang menetapkan public/ sebagai document root — kelas-kelas di src/ otomatis tidak bisa diakses langsung dari HTTP.
# terminal 1 — dari direktori root proyek
$ php -S 127.0.0.1:8094 -t publicServer berjalan di foreground dan mencatat setiap request yang masuk. Dari terminal kedua, kita memanggil endpoint dengan curl:
# terminal 2
$ curl -sS -i http://127.0.0.1:8094/api/kesehatanHTTP/1.1 200 OK
Host: 127.0.0.1:8094
Date: Tue, 13 Oct 2026 21:03:12 GMT
Connection: close
X-Powered-By: PHP/8.4.23
Content-Type: application/json; charset=utf-8
{"status":"ok"}Status 200, header Content-Type: application/json; charset=utf-8, dan body {"status":"ok"} — persis seperti hasil simulasi CLI sebelumnya, kali ini melalui jalur HTTP sungguhan. Baris Date akan menyesuaikan tanggal dan jam permintaan Anda. Lanjut ke path yang tidak ada:
$ curl -sS http://127.0.0.1:8094/api/halusinasi{"error":{"code":"NOT_FOUND","message":"Endpoint GET /api/halusinasi tidak ditemukan."}}Bentuk error {"error": {"code": ..., "message": ...}} yang kita pilih di sini bukan kebetulan — format ini akan menjadi kontrak tetap seluruh error API, dan pada Bagian 4 pembentuknya dipusatkan ke satu kelas ErrorHandler.
Apa yang Berubah #
Dibandingkan dengan sebelum proyek dimulai, tahap ini menambahkan hal-hal berikut:
composer.json— identitas package, dependensi, dan pemetaan autoloading PSR-4App\kesrc/.public/index.php— front controller dengan autoloader cadangan tanpa vendor, guard CLI yang ramah, satu endpointGET /api/kesehatan, dan fallback 404 berbentuk JSON.- Kontrak output: semua response adalah JSON dengan flag
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, dan semua error membungkus objekerrorberisicodedanmessage.
Belum ada routing sungguhan — dispatch masih berupa satu kondisi if. Bagian 2 menggantinya dengan kelas Router yang mengenali parameter {id}.
Cara Menjalankan #
Dari direktori rest-api-manajemen-tugas/ (atau folder bundle halaman ini), tiga cara memeriksa hasil kerja Bagian 1:
# 1. Petunjuk CLI — guard mencetak panduan lalu exit 0
php public/index.php
# 2. Web server bawaan, lalu panggil dari terminal lain
php -S 127.0.0.1:8094 -t public
curl -sS http://127.0.0.1:8094/api/kesehatan
# 3. Opsional: pasang dependensi dev lewat Composer
composer installMenjalankan composer install bersifat opsional — autoloader cadangan sudah menjamin API berjalan tanpa direktori vendor/. Dua berkas pendukung halaman ini, composer.json dan public/index.php, tersedia pada bagian bawah halaman dan dapat diunduh langsung.
Sebagai rujukan resmi: dokumentasi json_encode()
untuk flag-flag JSON, PHP CLI dan built-in web server
untuk perilaku php -S, serta dasar-dasar Composer
dan standar PSR-4
untuk autoloading. Dengan fondasi yang berdiri, kita lanjut ke Bagian 2 untuk membangun routing dan abstraksi request/response yang bisa diuji tanpa web server.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
composer.json
Unduh berkas{
"name": "budi/rest-api-manajemen-tugas",
"description": "REST API manajemen tugas dengan PHP murni, tanpa framework.",
"type": "project",
"license": "MIT",
"require": {
"php": ">=8.3"
},
"require-dev": {
"phpunit/phpunit": "^12.0"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"authors": [
{
"name": "Budi",
"email": "[email protected]"
}
],
"config": {
"sort-packages": true
}
}