Halaman HTML bukan satu-satunya bentuk jawaban aplikasi modern. Panggilan AJAX dari frontend JavaScript, aplikasi mobile, maupun layanan pihak ketiga meminta data terstruktur — dan format de facto-nya adalah JSON. Laravel membuat pengembalian JSON dari controller hampir tanpa gesekan: helper response()->json(), konversi otomatis dari array, sampai bentuk khusus seperti JSONP untuk script lintas domain dan response unduhan berkas. Chapter ini membangun endpoint statistik untuk mempraktikkan semuanya.

response()->json dengan Status dan Header #

Kita mulai dari controller statistik yang membaca data post. Helper response()->json() menerima data di argumen pertama, status code di argumen kedua, dan header tambahan bisa dirantai setelahnya:

php
<?php

namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;

class StatistikController extends Controller
{
    public function index(): JsonResponse
    {
        return response()->json([
            'total_post' => Post::count(),
            'post_terbaru' => Post::latest('id')->value('title'),
        ]);
    }

    public function simpan(Request $request): JsonResponse
    {
        $validated = $request->validate([
            'title' => 'required|string|max:100',
            'content' => 'required|string',
        ]);

        $post = Post::create($validated);

        return response()->json(['id' => $post->id, 'title' => $post->title], 201)
            ->header('Location', "/posts/{$post->id}");
    }
}

Method simpan() memperlihatkan pola API klasik: data valid disimpan, lalu dijawab 201 Created dengan header Location yang menunjuk sumber daya barunya — persis seperti konvensi REST yang akan kita dalami di Section API Development. Daftarkan keduanya lalu kita lihat bentuk jawabannya lewat runner:

php
<?php

// uji-json.php — jalankan dari root proyek Laravel
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Request;

$dir = $argv[1];
require $dir.'/vendor/autoload.php';
$app = require $dir.'/bootstrap/app.php';

$kernel = $app->make(Kernel::class);

$response = $kernel->handle(Request::create('http://localhost:8000/statistik', 'GET'));
echo 'GET /statistik → HTTP '.$response->getStatusCode().PHP_EOL;
echo 'Content-Type: '.$response->headers->get('Content-Type').PHP_EOL;
echo $response->getContent().PHP_EOL;

Output yang Dihasilkan:

text
GET /statistik → HTTP 200
Content-Type: application/json
{"total_post":2,"post_terbaru":"Belajar Response JSON"}

Helper json() otomatis mengatur Content-Type: application/json dan menyandikan array menjadi string JSON — dua pekerjaan yang seharusnya kita lakukan manual dengan json_encode().

Array Otomatis Menjadi JSON #

Bentuk yang lebih singkat lagi: mengembalikan array langsung dari method controller. Laravel mendeteksi tipe kembalian dan mengonversinya menjadi response JSON tanpa helper apa pun:

php
public function ringkas(): array
{
    return ['total_post' => Post::count()];
}

Request GET /statistik-ringkas kini menghasilkan body {"total_post":2} dengan status 200 — hasil yang identik dengan response()->json(['total_post' => ...]). Perbedaannya hanya gaya; tim biasanya memilih satu dan konsisten.

JSONP untuk Script Lintas Domain #

Sebelum CORS menjadi umum, script <script> lintas domain memakai teknik JSONP: JSON dibungkus dalam pemanggilan fungsi JavaScript bernama callback. Method withCallback() menghasilkan format tersebut:

php
public function jsonp(Request $request): JsonResponse
{
    return response()->json(['total_post' => Post::count()])
        ->withCallback($request->input('callback', 'panggilanBalik'));
}

Kita jalankan runner terhadap /statistik-jsonp?callback=panggilanBalik:

text
GET /statistik-jsonp?callback=panggilanBalik → HTTP 200
Content-Type: text/javascript; charset=utf-8
/**/panggilanBalik({"total_post":2});

Body-nya kini berupa pernyataan JavaScript yang memanggil fungsi panggilanBalik() dengan JSON sebagai argumennya, dan Content-Type berubah menjadi text/javascript. Endpoint JSONP masih kita jumpai di layanan lama, jadi mengenalinya lebih berharga daripada memakainya untuk API baru.

Unduhan Berkas #

Satu bentuk response khusus lagi yang sering diperlukan endpoint data: mengirim berkas sebagai lampiran unduhan. Method response()->download() menerima path berkas, nama yang diterima pengunduh, dan array header:

php
public function unduh(): BinaryFileResponse
{
    return response()->download(
        storage_path('app/laporan.txt'),
        'laporan.txt',
        ['Content-Type' => 'text/plain'],
    );
}

Browser yang menerima response tersebut akan menawarkan penyimpanan berkas bernama laporan.txt, bukan menampilkan isinya — perbedaan mendasar dari response HTML biasa. Method ini cocok untuk berkas yang memang sudah tersimpan di disk, misalnya hasil ekspor yang dibuat job malam hari; untuk data yang dibangkitkan saat itu juga, response()->streamDownload() mengalirkan isinya tanpa menulis berkas sementara lebih dulu.

Menguji Semua Bentuk JSON #

Feature test berikut memeriksa status, header, isi JSON, sampai unduhan:

php
<?php

namespace Tests\Feature;

use App\Models\Post;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class JsonResponseTest extends TestCase
{
    use RefreshDatabase;

    public function test_json_dari_response_helper(): void
    {
        Post::create(['title' => 'Belajar Controller', 'content' => 'Isi pertama.']);

        $this->get('/statistik')
            ->assertOk()
            ->assertJson([
                'total_post' => 1,
                'post_terbaru' => 'Belajar Controller',
            ]);
    }

    public function test_json_status_201_dengan_header(): void
    {
        $this->postJson('/statistik', [
            'title' => 'Belajar Response JSON',
            'content' => 'Konten dikirim sebagai JSON.',
        ])
            ->assertStatus(201)
            ->assertJson(['title' => 'Belajar Response JSON'])
            ->assertHeader('Location', '/posts/1');
    }

    public function test_array_otomatis_menjadi_json(): void
    {
        Post::create(['title' => 'Belajar Controller', 'content' => 'Isi pertama.']);

        $this->get('/statistik-ringkas')
            ->assertOk()
            ->assertJson(['total_post' => 1]);
    }

    public function test_jsonp_membungkus_json_dalam_callback(): void
    {
        Post::create(['title' => 'Belajar Controller', 'content' => 'Isi pertama.']);

        $this->get('/statistik-jsonp?callback=panggilanBalik')
            ->assertOk()
            ->assertSee('panggilanBalik(');
    }

    public function test_unduhan_berkas(): void
    {
        $this->get('/unduh-laporan')
            ->assertOk()
            ->assertDownload('laporan.txt');
    }

    public function test_error_juga_json_saat_client_meminta_json(): void
    {
        // matikan mode debug agar body error seringkas di production
        config(['app.debug' => false]);

        $this->getJson('/posts/999')
            ->assertNotFound()
            ->assertJsonPath('message', 'No query results for model [App\\Models\\Post] 999');
    }
}

Output yang Dihasilkan:

text
  PASS  Tests\Feature\JsonResponseTest
  ✓  json dari response helper                                             0.15s  
  ✓  json status 201 dengan header                                         0.01s  
  ✓  array otomatis menjadi json                                           0.01s  
  ✓  jsonp membungkus json dalam callback                                  0.01s  
  ✓  unduhan berkas                                                        0.01s  
  ✓  error juga json saat client meminta json                              0.01s  

  Tests:    6 passed (14 assertions)
  Duration: 0.27s

Assertion assertJson() cukup cerdas membandingkan sebagian isi — JSON boleh memiliki kunci lain selama kunci yang kita nyatakan cocok. Test terakhir memperlihatkan perilaku penting: ketika client mengirim header Accept: application/json (dibuat oleh $this->getJson()), error pun dijawab dalam format JSON, bukan halaman HTML error — dengan APP_DEBUG=false, body-nya ringkas berisi message. Seluruh ragam response JSON terdokumentasi di dokumentasi responses .

Kita kini menguasai isi controller, mulai dari konsep sampai bentuk response. Pertanyaan berikutnya bersifat struktural: ke mana semua controller ini kita susun ketika jumlahnya mencapai puluhan? Jawabannya adalah organisasi direktori dan namespace — topik chapter Organisasi Controller berikutnya.