JSON API
A file under public/api/ is an endpoint: response()->json($value) sends any
value as JSON, and objects contribute their public properties.
A shared counter
The button sends POST /api/clicks with fetch(). Everyone who visits shares the
total; the server also counts your clicks in your session.
Total 5 Yours 0
// the JSON response appears here
Read-only endpoints
- GET /api/clicks — the counter
- GET /api/messages — the message board as JSON
- GET /api/messages?limit=3
Source of this page
Read from disk by lib/source.phx as the page is served.
views/api.phx.html
<?phx params(int $total, int $yours) ?>
<h1>JSON API</h1>
<p class="lead">A file under <code>public/api/</code> is an endpoint: <code>response()->json($value)</code> sends any
value as JSON, and objects contribute their public properties.</p>
<div class="demo">
<h2>A shared counter</h2>
<p>The button sends <code>POST /api/clicks</code> with <code>fetch()</code>. Everyone who visits shares the
total; the server also counts your clicks in your session.</p>
<p class="counter"><button id="click">+1</button>
<span>Total <strong id="total"><?= $total ?></strong></span>
<span>Yours <strong id="mine"><?= $yours ?></strong></span></p>
<pre class="term"><code id="response">// the JSON response appears here</code></pre>
</div>
<script>
// This inline script runs because PHX adds a CSP nonce to scripts written in
// templates. The token comes from json_html(), the only way to put a value here.
const token = <?= json_html(csrf_token()) ?>;
const button = document.getElementById("click");
button.addEventListener("click", async () => {
button.disabled = true;
try {
const res = await fetch("/api/clicks", {
method: "POST",
headers: { "Content-Type": "application/json", "X-CSRF-Token": token },
body: "{}",
});
const data = await res.json();
document.getElementById("response").textContent = JSON.stringify(data, null, 2);
if (res.ok) {
document.getElementById("total").textContent = data.total;
document.getElementById("mine").textContent = data.yours;
}
} finally {
button.disabled = false;
}
});
</script>
<div class="demo">
<h2>Read-only endpoints</h2>
<ul>
<li><a href="/api/clicks">GET /api/clicks</a> — the counter</li>
<li><a href="/api/messages">GET /api/messages</a> — the message board as JSON</li>
<li><a href="/api/messages?limit=3">GET /api/messages?limit=3</a></li>
</ul>
</div>
public/api/clicks.phx
<?phx
import std.http.{request, response, session};
import lib.client.client_ip;
import lib.store.{demo_db, bump_counter, counter};
// GET /api/clicks -> {"total": 12}
// POST /api/clicks -> {"total": 13, "yours": 1}
$db = demo_db();
if (!request()->isPost()) {
response()->json(["total" => counter($db, "clicks")]);
return;
}
if (!rate_limit("clicks:" . client_ip(), 60, 60)) {
response()->status(429)->json(["error" => "Slow down: at most 60 clicks a minute."]);
return;
}
$yours = (session()->get("clicks")?->toInt() ?? 0) + 1;
session()->set("clicks", (string)$yours);
response()->json(["total" => bump_counter($db, "clicks"), "yours" => $yours]);
public/api/messages.phx
<?phx
import std.http.{request, response};
import lib.store.{demo_db, recent_messages};
// GET /api/messages?limit=10
$limit = request()->query("limit")?->toInt() ?? 30;
$limit = max(1, min($limit, 30));
response()->json(["messages" => recent_messages(demo_db(), $limit)]);