Membangun REST API production-grade dengan API Platform: menandai entity dengan ApiResource, mengatur operasi & filters, pagination & sorting, dokumentasi OpenAPI/Swagger otomatis, content negotiation JSON-LD/HAL, serta validasi input dari sisi API.

Di episode 12 aplikasi sudah aman. Episode 13 menjawab kebutuhan berikutnya yang tak terelakkan di 2026: API. SPA, mobile app, dan integrasi pihak ketiga semuanya butuh endpoint yang rapi, terdocument, dan konsisten. API Platform — yang dibangun di atas Symfony — mengubah pembuatan API dari pekerjaan manual menjadi deklaratif.
Mengapa API Platform, bukan menulis controller JSON manual? Karena API butuh banyak hal yang mudah lupa: dokumentasi OpenAPI, pagination, filter, validasi, format konten (JSON-LD/HAL), dan security. API Platform menyediakan semuanya dari entity yang sama dengan yang sudah kalian punya sejak episode 6.
composer require apiInstallasi ini mengaktifkan route /api, dokumentasi Swagger di /api/docs, dan integrasi dengan entity. Sekarang tandai entity sebagai resource API:
use ApiPlatform\Metadata\ApiResource;
#[ApiResource]
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
// kolom yang sudah ada sejak episode 6...
}Satu attribute #[ApiResource] sudah menghasilkan endpoint CRUD lengkap:
| Method | Route | Fungsi |
|---|---|---|
GET | /api/articles | Koleksi artikel |
POST | /api/articles | Buat artikel |
GET | /api/articles/{id} | Detail artikel |
PUT / PATCH | /api/articles/{id} | Update artikel |
DELETE | /api/articles/{id} | Hapus artikel |
Coba langsung di browser: buka /api/articles — kalian akan melihat JSON-LD dengan properti seperti @id, @type, dan hydra:member. Buka /api/docs untuk dokumentasi interaktif.
Tidak semua entity perlu seluruh operasi. Batasi lewat attribute:
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
#[ApiResource(
operations: [
new Get(),
new GetCollection(),
],
)]
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
}Dengan ini, /api/articles hanya read-only — tidak ada POST/PUT/DELETE. Ini contoh bagus bagaimana API Platform membuat kebijakan API terlihat eksplisit di deklarasi resource, bukan tersebar di kode.
Koleksi API tanpa filter tidak berguna di dataset nyata. Aktifkan filter bawaan:
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Doctrine\Orm\Filter\DateFilter;
use ApiPlatform\Metadata\ApiFilter;
#[ApiResource]
#[ApiFilter(SearchFilter::class, properties: ['title' => 'ipartial'])]
#[ApiFilter(DateFilter::class, properties: ['createdAt'])]
#[ORM\Entity(repositoryClass: ArticleRepository::class)]
class Article
{
}Query yang tersedia:
GET /api/articles?title=Symfony&order[createdAt]=desc&page=2
GET /api/articles?createdAt[before]=2026-08-01Pagination & sorting diatur global di config/packages/api_platform.yaml:
api_platform:
defaults:
pagination:
enabled: true
items_per_page: 15
maximum_items_per_page: 50
client_items_per_page: trueclient_items_per_page: true memperbolehkan klien meminta ?itemsPerPage=... dalam batas maksimum.
API Platform mengirim JSON-LD secara default. Request dengan header Accept yang berbeda mengubah format:
Accept: application/ld+json → JSON-LD (default)
Accept: application/hal+json → HAL
Accept: application/json → JSON polos
Accept: application/merge-patch+json → untuk PATCHNegosiasi ini penting: format adalah kontrak — SPA kalian bisa memilih mana yang paling nyaman tanpa mengubah kode server. Untuk JSON murni (tanpa hypermedia), set formats di resource:
api_platform:
defaults:
formats:
jsonld: ['application/ld+json']
json: ['application/json']
html: ['text/html']Constraint validation episode 7 langsung berlaku di API — tidak perlu menulis ulang:
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 255)]
private ?string $title = null;Kirim POST tanpa title → respons 422 Unprocessable Content dengan daftar violations yang rapi. Satu definisi validasi dipakai untuk form dan API — ini salah satu keunggulan terbesar.
Gabungkan dengan episode 12: proteksi resource per role.
#[ApiResource(
security: 'is_granted("ROLE_USER")',
operations: [
new GetCollection(),
new Get(),
new Post(security: 'is_granted("ROLE_USER")'),
new Delete(security: 'is_granted("ROLE_ADMIN")'),
],
)]Ungkapan security dievaluasi per operasi; securityPostDenormalize menangani kasus berbasis objek (misal hanya penulis yang bisa PUT). Response otomatis 403 saat tidak berwenang.
Tip
Mulai dari operasi yang paling sedikit dulu — read-only (GET) — lalu perluas. API yang terlalu terbuka lebih sulit ditutup daripada API yang tertutup lalu dibuka bertahap. Security lebih mudah ditambah ketika dideklarasikan sejak awal resource.
normalizationContext/denormalizationContext (misal groups: ['article:read']) agar properti internal (seperti hash password) tidak bocor.#[ApiFilter] atau didaftarkan di resource.hydra:view di respons.Pada episode 13 ini, kalian telah membangun API penuh di atas Symfony.
Inti yang harus dibawa pulang:
#[ApiResource] mengubah entity menjadi REST API CRUD lengkap.operations; filter via #[ApiFilter].security per operasi.Di episode 14 selanjutnya kita membuat aplikasi yang lebih cepat: Caching & Performance — PSR-6/16 cache pools (Redis, APCu), HTTP cache dengan Cache-Control & ESI, opcache, dan strategi invalidasi cache yang benar. Sampai jumpa di episode 14!