JSON API
Responses::json() encodes data as JSON and returns a PSR-7 response with the selected HTTP status and content type.
Registering an endpoint
AppApiEndpointProvider registers public GET status and POST preview operations with ApiEndpointRegistry. ControllerAction::for() identifies each controller action. These paths omit the prefix from Api.yaml, which is applied when the routes are built.
<?php
declare(strict_types=1);
namespace App\Api;
use App\Controllers\Api\PreviewController;
use App\Controllers\Api\StatusController;
use Lemonade\Framework\Api\Endpoint\ApiAccess;
use Lemonade\Framework\Api\Endpoint\ApiEndpointProviderInterface;
use Lemonade\Framework\Api\Endpoint\ApiEndpointRegistry;
use Lemonade\Framework\Routing\ControllerAction;
final class AppApiEndpointProvider implements ApiEndpointProviderInterface
{
public function register(ApiEndpointRegistry $registry): void
{
$registry->get(
path: '/app/status',
controllerAction: ControllerAction::for(StatusController::class, 'show'),
name: 'app.status',
summary: 'Application status',
description: 'Returns application availability.',
access: ApiAccess::Public,
);
$registry->post(
path: '/app/preview',
controllerAction: ControllerAction::for(PreviewController::class, 'create'),
name: 'app.preview',
summary: 'Preview title',
description: 'Returns a normalized title.',
access: ApiAccess::Public,
);
}
}Api.yaml enables the module, sets /api as its prefix, and lists the provider above. Once Config.yaml loads the API module, its two paths become /api/app/status and /api/app/preview.
module: api
config:
enabled: true
prefix: /api
endpoint_providers:
- App\Api\AppApiEndpointProviderResponse and status
StatusController injects Responses and returns a JSON payload under data from show(). Responses::json() creates a PSR-7 response with HTTP 200 by default; a different status, such as 201 or 422, can be passed as the second argument.
<?php
declare(strict_types=1);
namespace App\Controllers\Api;
use Lemonade\Framework\Http\Response\Responses;
use Psr\Http\Message\ResponseInterface;
final readonly class StatusController
{
public function __construct(private Responses $responses) {}
public function show(): ResponseInterface
{
return $this->responses->json(['data' => ['status' => 'ready']]);
}
}Input and errors
PreviewController reads title from RequestData::jsonPayload() and returns HTTP 422 unless it is a nonblank string. A valid title is trimmed and returned as JSON with HTTP 200; nothing is stored. jsonPayload() also returns an empty array for malformed JSON, so distinguishing that error from an empty object requires separate decoding and error handling.
<?php
declare(strict_types=1);
namespace App\Controllers\Api;
use Lemonade\Framework\Core\Http\RequestData;
use Lemonade\Framework\Http\Response\Responses;
use Psr\Http\Message\ResponseInterface;
final readonly class PreviewController
{
public function __construct(
private RequestData $request,
private Responses $responses,
) {}
public function create(): ResponseInterface
{
$title = $this->request->jsonPayload()['title'] ?? null;
if (!is_string($title) || trim($title) === '') {
return $this->responses->json(['error' => 'Title is required'], 422);
}
return $this->responses->json(['title' => trim($title)], 200);
}
}This is the JSON body returned for a missing or blank title. The 422 status comes from the controller response, not from a field in the payload.
{"error":"Title is required"}The request sends an empty JSON object to the registered POST endpoint. With no title present, the controller returns the 422 response shown above.
curl -i -X POST http://localhost/api/app/preview \
-H 'Content-Type: application/json' -d '{}'