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
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
// 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:
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:
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:
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:
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:
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
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:
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.27sAssertion 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.