Membangun API JSON

Header, json_encode, dan kode status respons.

Daftar isi

API JSON adalah endpoint yang menerima dan mengembalikan data dalam format JSON melalui protokol HTTP, dengan kode status sebagai penanda hasil dan Content-Type: application/json sebagai penanda isi. Topik ini membangun satu API tugas kecil dengan PHP murni, membungkus logika penanganannya dalam fungsi yang dapat diuji dari command line, lalu menyajikannya lewat server bawaan dan mengujinya dengan curl. Setelah topik ini, struktur respons yang benar berupa status, header, dan body terlihat utuh dalam keluaran nyata.

Respons JSON dan kode status#

Kode status menjawab pertanyaan apakah permintaan berhasil sebelum body dibaca, sedangkan body menyampaikan datanya. Aturan yang dipakai API di topik ini mengikuti kebiasaan umum REST, yaitu 200 untuk pembacaan berhasil, 201 untuk data baru, 404 untuk data yang tidak ditemukan, 422 untuk body yang tidak lengkap, dan 405 untuk metode yang tidak didukung. Kebiasaan yang sama menuntut bentuk body yang konsisten, yaitu data yang berhasil dikembalikan apa adanya, sedangkan kegagalan dikembalikan sebagai objek dengan kunci pesan. Konsistensi bentuk membuat client dapat menuliskan satu jalur penanganan error untuk semua kasus gagal. Berkas index.php di bawah ini memuat seluruhnya, dengan logika penanganan yang dipisahkan dari pengiriman agar dapat dipanggil tanpa server.

php index.php
<?php

declare(strict_types=1);

// data tugas disimpan dalam array; setiap permintaan memulai dari keadaan awal
$tugas = [
    ['id' => 1, 'judul' => 'Belanja mingguan', 'selesai' => false],
    ['id' => 2, 'judul' => 'Menyapu halaman', 'selesai' => true],
];

// mengirim satu respons JSON lengkap dengan kode statusnya
function respons_json(int $status, mixed $data): void
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), "\n";
}

function tangani(string $metode, string $jalur, array $param, string $isi, array $tugas): array
{
    if ($jalur !== '/tugas') {
        return [404, ['pesan' => 'Jalur tidak dikenal']];
    }

    if ($metode === 'GET' && !isset($param['id'])) {
        return [200, $tugas];
    }

    if ($metode === 'GET') {
        foreach ($tugas as $satu) {
            if ($satu['id'] === (int) $param['id']) {
                return [200, $satu];
            }
        }

        return [404, ['pesan' => 'Tugas tidak ditemukan']];
    }

    if ($metode === 'POST') {
        $data = json_decode($isi, true, 512, JSON_THROW_ON_ERROR);
        $judul = trim((string) ($data['judul'] ?? ''));
        if ($judul === '') {
            return [422, ['pesan' => 'Kolom judul wajib diisi']];
        }

        $baru = ['id' => count($tugas) + 1, 'judul' => $judul, 'selesai' => false];

        return [201, $baru];
    }

    return [405, ['pesan' => 'Metode tidak didukung']];
}

if (PHP_SAPI === 'cli') {
    // skenario demo: setiap kasus memakai keadaan awal yang sama
    $skenario = [
        ['GET', '/tugas', [], ''],
        ['GET', '/tugas', ['id' => '99'], ''],
        ['POST', '/tugas', [], '{"judul":"Menghapus debu rak"}'],
        ['POST', '/tugas', [], '{"judul":"  "}'],
    ];

    foreach ($skenario as [$metode, $jalur, $param, $isi]) {
        [$status, $data] = tangani($metode, $jalur, $param, $isi, $tugas);
        echo "HTTP $status", "\n";
        echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT), "\n\n";
    }

    return;
}

// mode web: permintaan nyata dari server bawaan
$jalur = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/';
$isi = file_get_contents('php://input') ?: '';

[$status, $data] = tangani($_SERVER['REQUEST_METHOD'], $jalur, $_GET, $isi, $tugas);
respons_json($status, $data);

Menjalankan berkas ini langsung dari command line dengan php index.php memicu skenario demo yang mencetak status dan body untuk setiap kasus, sehingga seluruh perilaku API terlihat tanpa server sama sekali.

text Keluaran
HTTP 200
[
    {
        "id": 1,
        "judul": "Belanja mingguan",
        "selesai": false
    },
    {
        "id": 2,
        "judul": "Menyapu halaman",
        "selesai": true
    }
]

HTTP 404
{
    "pesan": "Tugas tidak ditemukan"
}

HTTP 201
{
    "id": 3,
    "judul": "Menghapus debu rak",
    "selesai": false
}

HTTP 422
{
    "pesan": "Kolom judul wajib diisi"
}

Fungsi tangani() mengembalikan pasangan status dan data tanpa mencetak apa pun, sehingga skenario di mode CLI memanggilnya langsung dan mencetak hasilnya. Pola yang sama berlaku pada web, dengan penanganan menerima metode, jalur, parameter, dan isi permintaan dari $_SERVER, $_GET, dan php://input. Flag JSON_THROW_ON_ERROR membuat json_decode() melempar JsonException untuk body yang tidak sah, bukan mengembalikan null tanpa suara, dan JSON_UNESCAPED_UNICODE menjaga teks berbahasa Indonesia tetap terbaca.

Pemisahan antara keputusan dan pengiriman juga mempersempit peluang kesalahan. Satu-satunya jalur keluaran adalah respons_json(), sehingga data internal seperti isi berkas atau jejak eksekusi tidak ikut terkirim, dan setiap jawaban tetap berbentuk JSON yang dapat dipahami client. Pada API nyata, pengecualian yang tidak tertangani dibungkus penangan global yang menjawab 500 dengan objek pesan yang sama, bukan halaman HTML penuh detail internal, sebagaimana dibahas pada modul Pola Aplikasi Web.

Menyajikan lewat server bawaan#

Server bawaan dijalankan dengan index.php sebagai router, sehingga setiap permintaan masuk ke skrip yang sama. Pembacaan daftar dan pembacaan satu data dibedakan oleh parameter id, sedangkan penambahan memakai metode POST dengan body JSON. Flag JSON_PRETTY_PRINT pada contoh membuat body menjorok dan mudah dibaca saat pengembangan, sedangkan API produksi biasanya mengirim output rapat tanpa flag tersebut untuk menghemat byte yang dikirim pada setiap permintaan.

bash
# jalankan server API pada port 8140
# header Date dibuang dari keluaran karena nilainya bergantung waktu eksekusi
php -S localhost:8140 index.php >/dev/null 2>&1 &
sleep 1
# minta daftar tugas
curl -s -i http://localhost:8140/tugas | grep -v '^Date:'
kill %1
text Keluaran
HTTP/1.1 200 OK
Host: localhost:8140
Connection: close
X-Powered-By: PHP/8.3.27
Content-Type: application/json; charset=utf-8

[
    {
        "id": 1,
        "judul": "Belanja mingguan",
        "selesai": false
    },
    {
        "id": 2,
        "judul": "Menyapu halaman",
        "selesai": true
    }
]

Header respons menunjukkan susunan yang benar, yaitu status 200 pada baris pertama dan Content-Type: application/json; charset=utf-8 yang berasal dari panggilan header() di respons_json(). Tanpa header tersebut, sebagian client akan menafsirkan isi sebagai teks biasa meskipun bentuknya JSON.

bash
# server dijalankan ulang untuk meminta data yang tidak ada
php -S localhost:8140 index.php >/dev/null 2>&1 &
sleep 1
# minta tugas dengan id 99
curl -s -i "http://localhost:8140/tugas?id=99" | grep -v '^Date:'
kill %1
text Keluaran
HTTP/1.1 404 Not Found
Host: localhost:8140
Connection: close
X-Powered-By: PHP/8.3.27
Content-Type: application/json; charset=utf-8

{
    "pesan": "Tugas tidak ditemukan"
}
bash
# server dijalankan ulang untuk menambahkan data baru
php -S localhost:8140 index.php >/dev/null 2>&1 &
sleep 1
# kirim tugas baru dengan body JSON
curl -s -i -d '{"judul":"Menghapus debu rak"}' -H 'Content-Type: application/json' http://localhost:8140/tugas | grep -v '^Date:'
kill %1
text Keluaran
HTTP/1.1 201 Created
Host: localhost:8140
Connection: close
X-Powered-By: PHP/8.3.27
Content-Type: application/json; charset=utf-8

{
    "id": 3,
    "judul": "Menghapus debu rak",
    "selesai": false
}

Keadaan antar permintaan#

Data tugas pada topik ini disimpan dalam array biasa, sehingga setiap permintaan memulai dari keadaan awal yang sama. Respons POST berupa 201 dengan data yang baru dibuat dan bukan daftar yang bertambah, karena keadaan array tidak melintasi batas permintaan. Perilaku ini adalah konsekuensi model shared-nothing yang dibahas pada topik cara kerja PHP, dan inilah alasan basis data menjadi penyimpanan yang sesungguhnya pada API nyata.

Pemisahan itu justru memudahkan pengujian. Fungsi tangani() menerima keadaan sebagai argumen dan mengembalikan hasil sebagai nilai kembalian, sehingga seluruh kasus dapat diperiksa tanpa HTTP sama sekali sebagaimana ditunjukkan skenario CLI. Struktur semacam ini juga membuat pengujian PHPUnit pada modul ini dapat menjangkau logika API tanpa menjalankan server.

Kesalahan umum#

  • Mengirim kode 200 untuk semua hasil. Client yang membaca kode status tidak dapat membedakan keberhasilan dari kegagalan, sehingga penanganan error menjadi bertele-tele. Kembalikan status yang sesuai sejak respons pertama.
  • Lupa Content-Type: application/json. Body tetap berupa JSON, tetapi beberapa client dan alat pengujian menolaknya atau menampilkannya sebagai teks.
  • Memakai json_decode() tanpa JSON_THROW_ON_ERROR. Body yang rusak menghasilkan null dan skrip berlanjut dengan data kosong, sehingga bug tersembunyi di balik perilaku yang tampak wajar.
  • Menyisipkan keluaran lain sebelum header() dan http_response_code(). Header hanya dapat dikirim sebelum ada byte keluaran, dan pesan headers already sent muncul bila aturan itu dilanggar.

Ringkasan#

  • Respons API JSON tersusun atas kode status, header Content-Type: application/json, dan body hasil json_encode(), dengan JSON_THROW_ON_ERROR untuk body masukan yang tidak sah.
  • Fungsi penanganan yang menerima metode, jalur, parameter, dan isi lalu mengembalikan pasangan status dan data dapat diuji dari CLI tanpa server.
  • Server bawaan dengan index.php sebagai router menyajikan seluruh jalur, dan curl dengan opsi -i menampilkan header beserta body untuk pemeriksaan.
  • Array sebagai penyimpanan menegaskan model shared-nothing, sehingga penyimpanan nyata pada API adalah basis data, bukan variabel skrip.

Referensi#