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);
});| Request | What it does |
|---|---|
GET /api/posts | List of posts |
GET /api/posts/5 | One post |
POST /api/posts | Create a post |
DELETE /api/posts/5 | Delete 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
| Action | Success | Errors |
|---|---|---|
| List, view | 200 | 404 |
| Create | 201 | 422 |
| Delete | 204 | 404 |
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.