REST API Example
REST API Example
This example builds a small, versioned JSON API for managing products. It covers a full CRUD resource, route grouping for API versioning, consistent status codes, and basic input validation — the shape most real Hazaar APIs end up taking.
Project Structure
app/
├── Config/
│ └── app.php
├── Controllers/
│ └── Api/
│ └── V1/
│ └── Products.php
├── Models/
│ └── Product.php
└── route.php1. Configure the Application
<?php
// app/Config/app.php
return [
'development' => [
'router' => [
'type' => 'file',
'file' => 'route.php',
],
'db' => [
'type' => 'sqlite',
'file' => 'application.db',
],
],
];2. The Product Model
Create app/Models/Product.php. As in Using Databases, the model owns all database access so the controller stays focused on HTTP concerns:
<?php
namespace App\Models;
use Hazaar\DBI\Adapter;
class Product
{
protected Adapter $db;
public function __construct(?Adapter $db = null)
{
$this->db = $db ?? Adapter::create();
$this->db->exec('
CREATE TABLE IF NOT EXISTS product (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL
)
');
}
public function all(): array
{
return $this->db->table('product')->fetchAll();
}
public function find(int $id): array|false
{
return $this->db->table('product')->findOne(['id' => $id]);
}
public function create(array $data): int
{
$this->db->table('product')->insert($data);
return (int) $this->db->lastInsertId();
}
public function update(int $id, array $data): bool
{
return false !== $this->db->table('product')->update($data, ['id' => $id]);
}
public function delete(int $id): bool
{
return false !== $this->db->table('product')->delete(['id' => $id]);
}
}3. The Controller
Create app/Controllers/Api/V1/Products.php. It extends Basic, the controller base intended for API endpoints that return data directly rather than rendering a view (see Controller Responses):
<?php
namespace App\Controllers\Api\V1;
use App\Models\Product;
use Hazaar\Controller\Basic;
use Hazaar\Controller\Response\JSON;
class Products extends Basic
{
protected Product $products;
public function __construct(\Hazaar\Application\Request $request)
{
parent::__construct($request);
$this->products = new Product();
}
// GET /api/v1/products
public function index(): JSON
{
return new JSON($this->products->all());
}
// GET /api/v1/products/{id}
public function show(int $id): JSON
{
$product = $this->products->find($id);
if (false === $product) {
return new JSON(['error' => "No product with ID {$id}"], 404);
}
return new JSON($product);
}
// POST /api/v1/products
public function store(): JSON
{
$data = (array) $this->request->getJSONBody();
if (empty($data['name']) || !isset($data['price'])) {
return new JSON(['error' => 'name and price are required'], 422);
}
$id = $this->products->create(['name' => $data['name'], 'price' => $data['price']]);
return new JSON($this->products->find($id), 201);
}
// PUT /api/v1/products/{id}
public function update(int $id): JSON
{
if (false === $this->products->find($id)) {
return new JSON(['error' => "No product with ID {$id}"], 404);
}
$data = (array) $this->request->getJSONBody();
$this->products->update($id, $data);
return new JSON($this->products->find($id));
}
// DELETE /api/v1/products/{id}
public function destroy(int $id): JSON
{
if (false === $this->products->find($id)) {
return new JSON(['error' => "No product with ID {$id}"], 404);
}
$this->products->delete($id);
return new JSON(null, 204);
}
}4. Versioned Routes
Group the routes under /api/v1 using Router::group(). Grouping now, even with a single version, means adding a /api/v2 group later is a copy-paste away instead of a rewrite:
<?php
// app/route.php
use App\Controllers\Api\V1\Products;
use Hazaar\Application\Router;
Router::group('/api/v1', function ($api) {
$api->get('/products', [Products::class, 'index']);
$api->get('/products/{int:id}', [Products::class, 'show']);
$api->post('/products', [Products::class, 'store']);
$api->put('/products/{int:id}', [Products::class, 'update']);
$api->delete('/products/{int:id}', [Products::class, 'destroy']);
});See Routing for more on route parameters and other routing methods.
5. Try It Out
# Create a product
curl -s -X POST http://localhost:8080/api/v1/products \
-H 'Content-Type: application/json' \
-d '{"name": "Widget", "price": 9.99}'
# List products
curl -s http://localhost:8080/api/v1/products
# Fetch one
curl -s http://localhost:8080/api/v1/products/1
# Update
curl -s -X PUT http://localhost:8080/api/v1/products/1 \
-H 'Content-Type: application/json' \
-d '{"name": "Widget", "price": 12.99}'
# Delete
curl -s -X DELETE http://localhost:8080/api/v1/products/1 -o /dev/null -w '%{http_code}\n'6. Securing the API
A real API needs authentication on every write, at minimum. Apply middleware to the whole group in one place — see Middleware and Authentication for how to build and attach it:
Router::group('/api/v1', function ($api) {
$api->get('/products', [Products::class, 'index']);
$api->get('/products/{int:id}', [Products::class, 'show']);
$api->post('/products', [Products::class, 'store']);
$api->put('/products/{int:id}', [Products::class, 'update']);
$api->delete('/products/{int:id}', [Products::class, 'destroy']);
})->auth();Next Steps
- Using Databases — more on the DBI table API used by the model.
- Authentication — protect write endpoints with token-based auth.
- Middleware — add request logging, rate limiting, or API keys.
- Creating an MCP Server — expose the same kind of data as tools an AI agent can call.