Capstone: JSON API
The spec
The second capstone exposes the same blog as JSON: list posts, read one, create, delete. No templates this time, only status codes and JSON bodies. Four endpoints cover the whole surface.
GET /api/posts - list, GET /api/posts/id - read one
POST /api/posts - create, DELETE /api/posts/id - delete
API routes
API routes look like page routes but answer with JsonResponse instead of templates. Map each endpoint to one controller method, keep the method short, and let the status code tell the client what happened.
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Routing\Attribute\Route; class PostApiController { #[Route("/api/posts", methods: ["GET"])] public function list(PostRepository $posts): JsonResponse { $rows = []; foreach ($posts->findAll() as $post) { $rows[] = ["id" => $post->getId(), "title" => $post->getTitle()]; } return new JsonResponse($rows); } }
Serialize groups
Entities carry more fields than the API should show. Mark the public fields with a serialize group and enable only that group per endpoint, so private columns never leak into answers.
use Symfony\Component\Serializer\Attribute\Groups; class Post { #[Groups(["post:read"])] private string $title = ""; }
return $this->json($post, context: ["groups" => ["post:read"]]);
Test the endpoints
Test the API like a client would: call the endpoint, check the status, decode the JSON, assert the shape. Three assertions per endpoint catch most regressions before deploy.
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class PostApiTest extends WebTestCase { public function testListReturnsJson(): void { $client = static::createClient(); $client->request("GET", "/api/posts"); $this->assertResponseIsSuccessful(); $data = json_decode($client->getResponse()->getContent(), true); $this->assertIsArray($data); } }
Next: Modern Symfony tour
Article author: Arthur Isaev