Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

REST API

An API is an interface for programs: instead of HTML pages the application returns data as JSON, and a mobile app or JavaScript on another site reads it. REST is a widespread convention for such an API: each kind of data (a resource) has its own address, and the HTTP method names the action on it.

Resource routes

The posts API lives next to the site, in a separate /api/posts group. Add it to config/routes.php outside the site group — no CSRF check is needed here:

// config/routes.php
use App\Action\Api;
 
$app->group('/api/posts', function (RouteCollectorProxy $group) {
    $group->get('', Api\ListPostsAction::class);
    $group->get('/{id:[0-9]+}', Api\ShowPostAction::class);
    $group->post('', Api\CreatePostAction::class);
    $group->delete('/{id:[0-9]+}', Api\DeletePostAction::class);
});
RequestWhat it does
GET /api/postsList of posts
GET /api/posts/5One post
POST /api/postsCreate a post
DELETE /api/posts/5Delete a post

API actions go into a separate folder, src/Action/Api/, so they are not confused with page actions.

A JSON helper

The json() function from chapter one becomes a class:

// src/Support/Json.php
namespace App\Support;
 
use Psr\Http\Message\ResponseInterface as Response;
 
final class Json
{
    public static function write(Response $response, mixed $data, int $status = 200): Response
    {
        $response->getBody()->write(json_encode($data, JSON_UNESCAPED_UNICODE));
 
        return $response
            ->withHeader('Content-Type', 'application/json')
            ->withStatus($status);
    }
}

The list

// src/Action/Api/ListPostsAction.php
namespace App\Action\Api;
 
final class ListPostsAction
{
    public function __construct(private PostRepository $posts) {}
 
    public function __invoke(Request $request, Response $response): Response
    {
        return Json::write($response, ['data' => $this->posts->all()]);
    }
}

Open http://localhost:8080/api/posts — the browser shows the posts as JSON. The repository is the same one the site pages use: only the form of the response changed. ShowPostAction is built the same way, but calls find() and throws HttpNotFoundException when the post does not exist.

Creating

// src/Action/Api/CreatePostAction.php
public function __invoke(Request $request, Response $response): Response
{
    $data = (array) $request->getParsedBody();
    $errors = $this->validator->validate($data);
 
    if ($errors !== []) {
        return Json::write($response, ['errors' => $errors], 422);
    }
 
    $id = $this->posts->create(trim($data['title']), trim($data['body']));
 
    return Json::write($response, $this->posts->find($id), 201);
}

The data is validated by the same PostValidator as the form. A request from the terminal:

curl -X POST http://localhost:8080/api/posts \
  -H "Content-Type: application/json" \
  -d '{"title": "A post from the API", "body": "Text"}'

The answer is the created post with code 201. The JSON body is parsed by the addBodyParsingMiddleware() middleware.

Deleting

Add a delete() method to the repository that runs DELETE FROM posts WHERE id = ? and returns true if a row was removed. The action:

// src/Action/Api/DeletePostAction.php
public function __invoke(Request $request, Response $response, array $args): Response
{
    if (!$this->posts->delete((int) $args['id'])) {
        throw new HttpNotFoundException($request);
    }
 
    return $response->withStatus(204);
}

Code 204 means "done, no response body".

Errors as JSON

The custom error handler from earlier lessons renders an HTML page — useless to a client program. Add an address check to its function:

// config/middleware.php
if (str_starts_with($request->getUri()->getPath(), '/api/')) {
    $response = $app->getResponseFactory()->createResponse();
 
    return Json::write($response, ['error' => 'Request failed'], $status);
}

The check goes right after $status is computed. Now /api/posts/999 outside debug mode returns JSON with code 404.

Response codes

ActionSuccessErrors
List, view200404
Create201422
Delete204404

Common mistake

A response with code 200 and the body {"error": "..."} is a common slip: the client program takes the request for a success and tries to parse data that is not there. The status code is part of the response: return validation errors with 422 and a missing record with 404.

Thanks for reading!

Donate to the site — we work hard for you.