Controller

Respons HTML dan JSON beserta param.

Daftar isi

Controller adalah lapisan yang menerjemahkan permintaan HTTP menjadi respons. Topik ini membahas controller JSON dan controller HTML, beserta alur param dari jalur maupun query sampai ke fungsi aksi. Setelah membaca, fungsi aksi dapat ditulis dengan json/2 dan html/2, pola render/2 pada proyek nyata dipahami, dan perbedaannya dengan demo mandiri juga diketahui.

Fungsi aksi menerima conn dan param#

Sebuah controller Phoenix adalah modul biasa yang fungsinya dipanggil oleh router sebagai action. Setiap fungsi aksi menerima dua argumen, yaitu conn berisi keadaan permintaan dan params berisi param (params), yakni data yang dikirim pemanggil bersama permintaan. Param digabungkan dari tiga sumber menjadi satu map: segmen dinamis pada rute, seperti :id pada get "/api/catatan/:id", string query, dan tubuh permintaan pada metode POST. Karena digabung, params["id"] mengembalikan nilai yang sama, baik dari jalur maupun dari query.

Argumen params adalah map dengan kunci string, sehingga pencocokan pola pada tanda tangan fungsi dapat digunakan untuk memaksa bentuk param yang diharapkan. Fungsi show(conn, %{"id" => id}) hanya cocok apabila param id ada, dan itu menjadi bentuk penjagaan yang ringkas. Sebaliknya, argumen dengan pola _params menandakan aksi yang tidak membutuhkan param sama sekali.

Kedua argumen tersebut mengikuti satu kontrak yang sama di seluruh Phoenix. Fungsi aksi mewarisi keadaan conn dari pipeline sebelumnya, lalu keadaan hasil kerjanya mengalir kembali setelah respons dikirim. Karena itu, aksi yang ditulis rapi biasanya pendek: membaca param, memanggil context, lalu mengirim respons dengan json/2, html/2, atau render/2.

Respons JSON dengan json/2#

Fungsi json/2 menyandikan map Elixir menjadi JSON, menyetel header content-type ke application/json, dan mengirimkan respons dalam satu panggilan. Fungsi ini mengembalikan conn yang sudah selesai, dan conn itulah yang menjadi nilai akhir fungsi aksi. Demo berikut menggunakan satu skrip utuh yang memuat router, controller, tiga permintaan, dan satu respons HTML.

elixir catatan_controller.exs
Mix.install([{:phoenix, "~> 1.8"}, {:jason, "~> 1.4"}])
Logger.configure(level: :warning)

defmodule CatatanController do
  use Phoenix.Controller, formats: [:json, :html]

  def index(conn, params) do
    data = [%{id: 1, judul: "Puisi hujan"}]
    json(conn, %{data: data, q: params["q"]})
  end

  def show(conn, %{"id" => id}) do
    json(conn, %{id: id, judul: "Puisi hujan"})
  end

  def unduh(conn, _params) do
    html(conn, "<h1>Catatan belajar</h1>")
  end
end

defmodule CatatanRouter do
  use Phoenix.Router

  pipeline :api do
    plug :accepts, ["json"]
  end

  pipeline :browser do
    plug :accepts, ["html"]
  end

  scope "/api" do
    pipe_through :api

    get "/catatan", CatatanController, :index
    get "/catatan/:id", CatatanController, :show
  end

  scope "/" do
    pipe_through :browser

    get "/catatan/unduh", CatatanController, :unduh
  end
end

# permintaan dengan query param
conn =
  Phoenix.ConnTest.build_conn(:get, "/api/catatan", %{"q" => "puisi"})
  |> CatatanRouter.call(CatatanRouter.init([]))

IO.inspect(conn.status, label: "status")
IO.inspect(Jason.decode!(conn.resp_body), label: "isi")

# permintaan dengan param jalur
conn =
  Phoenix.ConnTest.build_conn(:get, "/api/catatan/3", nil)
  |> CatatanRouter.call(CatatanRouter.init([]))

IO.inspect(conn.status, label: "status show")
IO.inspect(Jason.decode!(conn.resp_body), label: "isi show")

# respons HTML
conn =
  Phoenix.ConnTest.build_conn(:get, "/catatan/unduh", nil)
  |> CatatanRouter.call(CatatanRouter.init([]))

IO.inspect(conn.status, label: "status html")
IO.inspect(conn.resp_body, label: "isi html")

Jalankan skripnya dengan elixir catatan_controller.exs. Tiga permintaan menghasilkan keluaran berikut.

text Keluaran
status: 200
isi: %{"data" => [%{"id" => 1, "judul" => "Puisi hujan"}], "q" => "puisi"}
status show: 200
isi show: %{"id" => "3", "judul" => "Puisi hujan"}
status html: 200
isi html: "<h1>Catatan belajar</h1>"

Keluaran tersebut memuat dua fakta yang perlu digarisbawahi. Pertama, query param q dari build_conn(:get, "/api/catatan", %{"q" => "puisi"}) sampai ke aksi index sebagai params["q"]. Kedua, param jalur :id selalu berupa string, sehingga %{"id" => "3"} berisi "3", bukan angka. Konversi ke bilangan bulat dilakukan sendiri dengan String.to_integer/1 pada waktu yang tepat, biasanya di context.

Fungsi json/2 berhenti setelah satu kali pengiriman. Penambahan header atau status setelah memanggilnya tidak berpengaruh, karena tubuh respons sudah ditetapkan. Apabila status berbeda dari 200 diperlukan, fungsi put_status/2 dipanggil sebelum json/2, dan urutan ini berlaku untuk fungsi respons lain juga.

Respons HTML dengan html/2#

Fungsi html/2 mengirimkan string HTML sebagai tubuh respons dan menyetel header content-type ke text/html; charset=utf-8. Aksi unduh pada demo di atas menggunakan fungsi ini dan tetap melewati pipeline :browser yang menegaskan format HTML melalui plug :accepts, ["html"]. Untuk halaman statis sederhana, html/2 sudah cukup dan tidak menuntut berkas template apa pun.

Perbedaan html/2 dengan render/2 terletak pada sumber tubuh respons. html/2 menerima string yang sudah jadi, sedangkan render/2 menyusun string tersebut dari template HEEx beserta nilai yang dikirim. Dua fungsi tersebut sama-sama berakhir sebagai conn dengan tubuh respons, sehingga pemilihan di antara keduanya adalah perkara sumber isi, bukan perkara arsitektur. Karena itu, html/2 juga wajar digunakan untuk respons kecil di luar halaman, misalnya tubuh teks sederhana atau isi berkas yang sudah tersusun.

Pola render/2 pada proyek nyata#

Proyek hasil mix phx.new menulis controller dengan render/2. Controller menyebut nama template, lalu Phoenix mencari modul view yang cocok secara otomatis berdasarkan nama controller. Selanjutnya, template HEEx pada modul view tersebut merender halaman. Potongan berikut diambil dari proyek Phoenix 1.8:

elixir Potongan PageController hasil generator Phoenix 1.8
# tidak-dijalankan (fragmen proyek, bukan skrip utuh)
defmodule NamaProyekWeb.PageController do
  use NamaProyekWeb, :controller

  def home(conn, _params) do
    render(conn, :home)
  end
end
elixir Potongan PageHTML hasil generator Phoenix 1.8
# tidak-dijalankan (fragmen proyek, bukan skrip utuh)
defmodule NamaProyekWeb.PageHTML do
  use NamaProyekWeb, :html

  embed_templates "page_html/*"
end

Makro use NamaProyekWeb, :controller menyiapkan banyak hal sekaligus, salah satunya pemetaan format ke modul view. Pada render(conn, :home), Phoenix meneruskan pekerjaan ke PageHTML dan template home.html.heex di dalam folder page_html. Konvensi penamaannya tegas: controller bernama PageController dipadukan dengan view bernama PageHTML, dan hubungan ini diturunkan dari nama modul tanpa konfigurasi tambahan. Pembagian ini menjaga controller tetap ramping: logika pemilihan data ada di controller, sedangkan penyusunan tampilan ada di template. Detail template HEEx dibahas pada topik Template dan layout.

Apabila modul view perlu ditentukan secara eksplisit, fungsi put_view/2 dipanggil sebelum render/2. Pola ini berguna untuk merender view yang tidak cocok dengan nama controller, misalnya modul view bersama untuk beberapa controller.

Kesalahan umum#

Kesalahan pertama adalah menganggap param jalur berupa bilangan. Nilai params["id"] selalu string, sehingga perbandingan langsung dengan bilangan bulat selalu gagal dan query database dapat salah sasaran. Perbaikannya, konversi secara eksplisit dengan String.to_integer/1, atau tangkap error ArgumentError apabila nilai tidak layak dikonversi.

Kesalahan kedua adalah lupa mengembalikan conn. Fungsi aksi wajib mengembalikan conn yang sudah berisi respons, dan hasil json/2, html/2, maupun render/2 semuanya adalah conn. Fungsi aksi yang berakhir dengan perhitungan lain, misalnya map, membuat Phoenix melempar error yang menyatakan bahwa fungsi aksi harus mengembalikan Plug.Conn. Pola pipa yang berakhir pada json(conn, data) menghindari kesalahan ini.

Kesalahan ketiga adalah mencampur format tanpa garis batas. Aksi pada scope pipe_through :api yang mengirim HTML akan membingungkan konsumen API, dan sebaliknya, JSON yang dikirim pada rute halaman HTML rusak di browser. Tata format pada tingkat scope, dan pastikan :accepts berjalan paling depan di setiap pipeline.

Ringkasan#

  • Fungsi aksi menerima conn dan params, lalu wajib mengembalikan conn yang sudah membawa respons.
  • Param digabung dari jalur, query, dan tubuh permintaan, dengan kunci berupa string, termasuk param jalur seperti :id.
  • json/2 mengirim JSON dan html/2 mengirim string HTML, keduanya menyetel header content-type secara otomatis.
  • Proyek nyata menggunakan render/2 yang merujuk modul view dan template HEEx, sedangkan put_view/2 menyebut modul view secara eksplisit apabila perlu.

Referensi#