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:

text
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.

json
{
    "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
<?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:

  1. Blok 1 (autoloading): dirname(__DIR__) menghasilkan path root proyek karena berkas ini berada satu tingkat di dalam public/. Bila vendor/autoload.php ada, Composer yang mengurus semuanya; bila tidak ada, spl_autoload_register() memasang closure yang memotong prefix App\, mengubah sisa nama kelas menjadi path, lalu require berkasnya bila memang ada.
  2. Blok 2 (handler): handleKesehatan() mengembalikan array murni — tidak menyentuh echo maupun header(). Pemisahan ini membuat handler mudah dipanggil langsung dari skrip pengujian. Fungsi kirimJson() adalah satu-satunya tempat yang berurusan dengan protokol HTTP.
  3. Blok 3 (guard CLI): konstanta PHP_SAPI bernilai "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 dengan exit(0) — bukan pesan error yang menakutkan.
  4. Blok 4 (dispatch): parse_url(..., PHP_URL_PATH) mengambil path bersih dari URI lengkap (membuang query string), lalu trim() menormalkan garis miring berlebih. Cocoknya method dan path dievaluasi dengan satu kondisi if; semua yang lain jatuh ke response 404 berbentuk JSON.

Guard CLI bisa kita lihat langsung hasilnya tanpa web server:

php
<?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:

text
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 layanan

Endpoint 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
<?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:

text
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
<?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:

text
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
<?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:

text
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.

bash
# terminal 1 — dari direktori root proyek
$ php -S 127.0.0.1:8094 -t public

Server berjalan di foreground dan mencatat setiap request yang masuk. Dari terminal kedua, kita memanggil endpoint dengan curl:

bash
# terminal 2
$ curl -sS -i http://127.0.0.1:8094/api/kesehatan
text
HTTP/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:

bash
$ curl -sS http://127.0.0.1:8094/api/halusinasi
text
{"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-4 App\ ke src/.
  • public/index.php — front controller dengan autoloader cadangan tanpa vendor, guard CLI yang ramah, satu endpoint GET /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 objek error berisi code dan message.

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:

bash
# 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 install

Menjalankan 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
json
{
    "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
    }
}