Webservices: the /api pattern
REST hands you four blunt verbs, a zoo of 37 versioned endpoints, and a DELETE that returns nothing useful. Tiger sidesteps it. The philosophy (from Beau Beauchamp's "REST is Dying"): your use case should drive the technology, not the other way around.
One endpoint. The message says where it's going.
There is one endpoint — POST /api — and the routing lives inside the message:
await fetch('/api', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
module: 'billing', // → Billing_Service_Invoice
service: 'invoice',
method: 'create', // → its create($params) method
amount: '4200', // …the payload just rides along
}),
});
No path to design. No verb to argue about. No new route to register. The message is the argument.
Add a method → you shipped an endpoint
A service extends Tiger_Service_Service. The method the message names receives the whole payload and writes the response:
class Billing_Service_Invoice extends Tiger_Service_Service
{
public function create(array $params): void
{
if (!$this->_isAdmin()) { $this->_error('billing.error.denied'); return; }
$form = new Billing_Form_Invoice();
if (!$form->isValid($params)) { $this->_formErrors($form); return; } // inline field errors, free
$id = $this->_transaction(function ($db) use ($params) {
// …inserts/updates; throw to abort + roll back…
return $newId;
});
$this->_success(['id' => $id], 'billing.invoice.created');
}
}
That's the whole endpoint. No routing file, no controller, no OpenAPI ceremony. And you got a lot for free: every call is ACL-gated (resource = the class, privilege = the method name), validated, and transactional. The canonical flow is always validate a form → wrap mutations in a transaction.
One response, always the same shape
Every call returns the same envelope — a real contract between client and server:
{ "result": 1,
"data": { "id": "019f…" },
"redirect": null,
"form": null,
"messages": [ { "message": "Invoice created.", "class": "success", "field": null } ] }
result is 1/0. messages[].class maps straight to a Bootstrap alert context, so the client renders feedback without inspecting the content. Field errors land in form. Redirects, inline validation, DataTables grids ({draw, recordsTotal, …, data}) — all the same envelope.
The phenomenal cosmic power
Because routing lives in the message and the response is a fixed contract, the same /api feeds every front end: the SSR theme you ship today, a React SPA tomorrow, a mobile app next quarter. The UI is always just a client. Add a method, and it's live for all of them — no endpoint zoo to maintain, no versions to juggle, less code to write and break. 🧞
See also
- API discovery — the OpenAPI catalog generated from your
/apisurface. - API tokens — authenticating a stateless, non-browser client (
Bearer). - Authorization — the deny-by-default ACL that gates every service.