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.

app/Api/AppApiEndpointProvider.php
<?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.

app/Config/Api.yaml
module: api
config:
  enabled: true
  prefix: /api
  endpoint_providers:
    - App\Api\AppApiEndpointProvider

Response 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.

StatusController.php
<?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.

app/Controllers/Api/PreviewController.php
<?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.

HTTP 422
{"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.

Terminal
curl -i -X POST http://localhost/api/app/preview \
  -H 'Content-Type: application/json' -d '{}'
Related documentationRead the JSON API chapter in the documentation for more detail.

← All examples