API Platform is one of the most efficient framework out there to create web APIs. It makes it easy to start creating APIs with the support of industry-leading open standards, while giving you the flexibility to build complex features. To discover the basics, we will create an API to manage a bookshop.
In a few minutes and just 2 steps, we will create a fully featured API:
API Platform uses these model classes to expose a web API having a ton of built-in features:
One more thing, before we start: API Platform is built on top of the Symfony framework. API Platform is compatible with most Symfony bundles (plugins) and benefits from the numerous extensions points provided by this rock-solid foundation (events, DIC…). Adding features like custom, service-oriented, API endpoints, JWT or OAuth authentication, HTTP caching, mail sending or asynchronous jobs to your APIs is straightforward.
API Platform is shipped with a Docker setup that makes it easy to get a containerized development environment up and running. This setup contains a Docker Compose configuration with several pre-configured and ready-use services with everything needed to run API Platform:
Name | Description | Port(s) |
---|---|---|
app | The application with PHP and PHP-FPM 7.1, the latest Composer | N/A |
db | A database provided by MySQL 5.7 | N/A |
nginx | An HTTP server provided by Nginx 1.11 | 8080 |
varnish | An HTTP cache provided by Varnish 4.1 | 80 |
Start by downloading the API Platform Standard Edition archive and extract its content. The resulting directory contains an empty API Platform project structure. You will add your own code and configuration inside it. Then, if you do not already have Docker on your computer, it’s the right time to install it.
Open a terminal, and navigate to the directory containing your project skeleton. Then, run the following command to start all services using Docker Compose:
$ docker-compose up -d # Running in detached mode
If you encounter problems running Docker on Windows (especially with Docker Toolbox), see our Troubleshooting guide.
The first time you start the containers, Docker downloads and builds images for you. It will take some time, but don’t worry, this is done only once. Starting servers will then be lightning fast.
In order to see container’s logs you will have to do:
$ docker-compose logs -f # follow the logs
Project’s files are automatically shared between your local host machine and the container thanks to a pre-configured Docker volume. It means that you can edit files of your project locally using your IDE or code editor, they will be transparently taken into account in the container. Speaking about IDEs, our favorite software to develop API Platform apps is PHPStorm with its awesome Symfony and PHP annotations plugins. Give them a try, you’ll got auto-completion for almost everything.
The API Platform Standard Edition comes with a dummy entity for test purpose: src/AppBundle/Entity/Foo.php
. We will remove
it later, but for now, create the related database table:
$ docker-compose exec app bin/console doctrine:schema:create
The app
container is where your project stands. Prefixing a command by docker-compose exec app
allows to execute the
given command in the container. You may want to create an alias to easily run commands
inside the container.
If you’re used to the PHP ecosystem, you probably guessed that this test entity uses the industry-leading Doctrine ORM library as persistence system. API Platform is 100% independent of the persistence system and you can use the one(s) that best suit(s) your needs (like a NoSQL database or a remote web service). API Platform even supports using several persistence systems together in the same project.
However, Doctrine ORM is definitely the easiest way to persist and query data in an API Platform project thanks to a bridge included in the Standard Edition. This Doctrine ORM bridge is optimized for performance and development convenience. Doctrine ORM and its bridge supports major RDBMS including MySQL, PostgreSQL, SQLite, SQL Server and MariaDB.
Instead of using Docker, API Platform can also be installed on the local machine using Composer:
$ composer create-project api-platform/api-platform bookshop-api
Then, enter the project folder, create the database and its schema:
$ cd bookshop-api
$ bin/console doctrine:database:create
$ bin/console doctrine:schema:create
And start the server:
$ bin/console server:run
Open http://localhost
with your favorite web browser:
API Platform exposes a description of the API in the Swagger format. It also integrates Swagger UI, a nice interface rendering
the API documentation. Click on an operation to display its details. You can also send requests to the API directly from the UI.
Try to create a new Foo resource using the POST
operation, then access it using the GET
operation and, finally, delete
it by executing the DELETE
operation.
If you access any API URL using a web browser, API Platform detects it (using the Accept
HTTP header) and displays the
corresponding API request in the UI. Try yourself by browsing to http://localhost/foos
.
If you want to access the raw data, you have two alternatives:
Accept
header (or don’t set any Accept
header at all and API Platform will default to JSON-LD) - preferred
when writing API clientsFor instance, go to http://localhost/foos.jsonld
to retrieve the list of Foo
resources in JSON-LD or http://localhost/foos.json
to retrieve data in raw JSON.
Of course, you can also use your favorite HTTP client to query the API. We strongly recommend to use Postman. It works perfectly well with API Platform, also has native Swagger support, allows to easily write functional tests and has good team collaboration features.
API Platform is now 100% functional. Let’s create our own data model.
Our bookshop API will start simple. It will be composed of a Book
resource type and a Review
one.
Books have an id, an ISBN, a title, a description, an author, a publication date and are related to a list of reviews. Reviews have an id, a rating (between 0 and 5), a body, an author, a publication date and are related to one book.
Let’s describe this data model as a set of Plain Old PHP Objects (POPO) and map it to database’s tables using annotations provided by the Doctrine ORM:
<?php
// src/AppBundle/Entity/Book.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* A book.
*
* @ORM\Entity
*/
class Book
{
/**
* @var int The id of this book.
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string|null The ISBN of this book (or null if doesn't have one).
*
* @ORM\Column(nullable=true)
*/
private $isbn;
/**
* @var string The title of this book.
*
* @ORM\Column
*/
private $title;
/**
* @var string The description of this book.
*
* @ORM\Column(type="text")
*/
private $description;
/**
* @var string The author of this book.
*
* @ORM\Column
*/
private $author;
/**
* @var \DateTimeInterface The publication date of this book.
*
* @ORM\Column(type="datetime")
*/
private $publicationDate;
/**
* @var Review[] Available reviews for this book.
*
* @ORM\OneToMany(targetEntity="Review", mappedBy="book")
*/
private $reviews;
}
<?php
// src/AppBundle/Entity/Review.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* A review of a book.
*
* @ORM\Entity
*/
class Review
{
/**
* @var int The id of this review.
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var int The rating of this review (between 0 and 5).
*
* @ORM\Column(type="smallint")
*/
private $rating;
/**
* @var string the body of the review.
*
* @ORM\Column(type="text")
*/
private $body;
/**
* @var string The author of the review.
*
* @ORM\Column
*/
private $author;
/**
* @var \DateTimeInterface The date of publication of this review.
*
* @ORM\Column(type="datetime")
*/
private $publicationDate;
/**
* @var Book The book this review is about.
*
* @ORM\ManyToOne(targetEntity="Book", inversedBy="reviews")
*/
private $book;
}
As you can see there are two typical PHP objects with the corresponding PHPDoc (note that entities’s and properties’s descriptions included in their PHPDoc will appear in the API documentation).
Doctrine’s annotations map these entities to tables in the MySQL database. Annotations are convenient as they allow grouping the code and the configuration but, if you want to decouple classes from their metadata, you can switch to XML or YAML mappings. They are supported as well.
Learn more about how to map entities with the Doctrine ORM in the project’s official documentation or in Kévin’s book “Persistence in PHP with the Doctrine ORM”.
As we used private properties (but API Platform as well as Doctrine can also work with public ones), we need to create the corresponding accessor methods. Run the following command or use the code generation feature of your IDE to generate them:
$ docker-compose exec app bin/console doctrine:generate:entities AppBundle
Then, delete the file src/AppBundle/Entity/Foo.php
, this demo entity isn’t useful anymore.
Finally, tell Doctrine to sync the database’s tables structure with our new data model:
$ docker-compose exec app bin/console doctrine:schema:update --force
We now have a working data model that you can persist and query. To create an API endpoint with CRUD capabilities corresponding
to an entity class, we just have to mark it with an annotation called @ApiResource
:
<?php
// src/AppBundle/Entity/Book.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
/**
* A book.
*
* @ApiResource
* @ORM\Entity
*/
class Book
{
// ...
}
<?php
// src/AppBundle/Entity/Review.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
/**
* A review of a book.
*
* @ApiResource
* @ORM\Entity
*/
class Review
{
// ...
}
Our API is (almost) ready!
Browse http://localhost/app_dev.php
to load the development environment (including the awesome Symfony profiler).
Operations available for our 2 resources types appear in the UI.
Click on the POST
operation of the Book
resource type and send the following JSON document as request body:
{
"isbn": "9781782164104",
"title": "Persistence in PHP with the Doctrine ORM",
"description": "This book is designed for PHP developers and architects who want to modernize their skills through better understanding of Persistence and ORM.",
"author": "Kévin Dunglas",
"publicationDate": "2013-12-01"
}
You just saved a new book resource through the bookshop API! API Platform automatically transforms the JSON document to an instance of the corresponding PHP entity class and uses Doctrine ORM to persist it in the database.
By default, the API supports GET
(retrieve, on collections and items), POST
(create), PUT
(update) and DELETE
(self-explaining)
HTTP methods. You are not limited to the built-in operations. You can add new custom operations
(PATCH
operations, sub-resources…) or disable the ones you don’t want.
Try the GET
operation on the collection. The book we added appears. When the collection will contain more than 30 items,
the pagination will automatically show up, and this is entirely configurable. You may be interested
in adding some filters and adding sorts to the collection as well.
You may have noticed that some keys start with the @
symbol in the generated JSON response (@id
, @type
, @context
…)?
API Platform comes with a full support of the JSON-LD format (and its Hydra
extension). It allows to build smart clients, with auto-discoverability capabilities (take a look at Hydra console)
and is useful for open data, SEO and interoperability when used with open vocabularies such as Schema.org.
JSON-LD enables a lot of awesome advanced features (like giving access to Google to your structured data
or consuming APIs with Apache Jena).
We think that it’s the best default format for a new API. However, API Platform natively supports many other formats
including HAL, raw JSON, XML
(experimental) and even YAML and CSV (if you
use Symfony 3.2+).
It’s up to you to choose which format to enable and to use by default. You can also easily add support for other formats
if you need to.
Now, add a review for this book using the POST
operation for the Review
resource:
{
"book": "/books/1",
"rating": 5,
"body": "Interesting book!",
"author": "Kévin",
"publicationDate": "September 21, 2016"
}
There are two interesting things to mention about this request:
First, we learned how to work with relations. In a hypermedia API, every resource is identified by an (unique) IRI.
A URL is a valid IRI, and it’s what API Platform uses. The @id
property of every JSON-LD document contains the IRI identifying
it. You can use this IRI to reference this document from other documents. In the previous request, we used the IRI of the
book we created earlier to link it with the Review
we were creating. API Platform is smart enough to deal with IRIs.
By the way, you may want to embed documents instead of referencing them
(e.g. to reduce the number of HTTP requests).
The other interesting thing is how API Platform handles dates (the publicationDate
property). API Platform understands
any date format supported by PHP. In production we strongly recommend
to use the format specified by the RFC 3339, but, as you can see, most common formats
including September 21, 2016
can be used.
To summarize, if you want to expose any entity you just have to:
Entity
directory of a bundle@ApiPlatform\Core\Annotation\ApiResource
annotationHow can it be more easy?!
Now try to add another book by issuing a POST
request to /books
with the following body:
{
"isbn": "2815840053",
"description": "Hello",
"author": "Me",
"publicationDate": "today"
}
Oops, we missed to add the title. But submit the request anyway. You should get a 500 error with the following message:
An exception occurred while executing 'INSERT INTO book [...] VALUES [...]' with params [...]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'title' cannot be null
Did you notice that the error was automatically serialized in JSON-LD and respect the Hydra Core vocabulary for errors? It allows the client to easily extract useful information from the error. Anyway, it’s bad to get a SQL error when submitting a request. It means that we didn’t use a valid input, and it’s a bad and dangerous practice.
API Platform comes with a bridge with the Symfony Validator Component. Adding some of its numerous validation constraints (or creating custom ones) to our entities is enough to validate user submitted data. Let’s add some validation rules to our data model:
<?php
// src/AppBundle/Entity/Book.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* A book.
*
* @ApiResource
* @ORM\Entity
*/
class Book
{
/**
* @var int The id of this book.
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var string|null The ISBN of this book (or null if doesn't have one).
*
* @ORM\Column(nullable=true)
* @Assert\Isbn
*/
private $isbn;
/**
* @var string The title of this book.
*
* @ORM\Column
* @Assert\NotBlank
*/
private $title;
/**
* @var string The description of this book.
*
* @ORM\Column(type="text")
* @Assert\NotBlank
*/
private $description;
/**
* @var string The author of this book.
*
* @ORM\Column
* @Assert\NotBlank
*/
private $author;
/**
* @var \DateTimeInterface The publication date of this book.
*
* @ORM\Column(type="datetime")
* @Assert\NotNull
*/
private $publicationDate;
/**
* @var Review[] Available reviews for this book.
*
* @ORM\OneToMany(targetEntity="Review", mappedBy="book")
*/
private $reviews;
// ...
}
<?php
// src/Entity/Review.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* A review of a book.
*
* @ApiResource
* @ORM\Entity
*/
class Review
{
/**
* @var int The id of this review.
*
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @var int The rating of this review (between 0 and 5).
*
* @ORM\Column(type="smallint")
* @Assert\Range(min=0, max=5)
*/
private $rating;
/**
* @var string the body of the review.
*
* @ORM\Column(type="text")
* @Assert\NotBlank
*/
private $body;
/**
* @var string The author of the review.
*
* @ORM\Column
* @Assert\NotBlank
*/
private $author;
/**
* @var \DateTimeInterface The date of publication of this review.
*
* @ORM\Column(type="datetime")
* @Assert\NotBlank
*/
private $publicationDate;
/**
* @var Book The book this review is about.
*
* @ORM\ManyToOne(targetEntity="Book", inversedBy="reviews")
* @Assert\NotNull
*/
private $book;
// ...
}
After updating the entities by adding those @Assert\*
annotations (as for Doctrine, you can use XML or YAML formats if you
prefer), try again the previous POST
request.
isbn: This value is neither a valid ISBN-10 nor a valid ISBN-13.
title: This value should not be blank.
You now get proper validation error messages, always serialized using the Hydra error format (API Problem is also supported). Those errors are easy to parse client-side. By adding the proper validation constraints, we also noticed that the provided ISBN isn’t valid…
Here we are! We have created a working and powerful hypermedia REST API in a few minutes, and by writing only a few lines of PHP. But we only covered the basics.
API Platform also provides amazing client-side components. Continue by creating a fancy Material Design administration interface for your API in seconds. Then, scaffold a ReactJS / Redux Progressive Web App.
They are many more features to learn! Read the full documentation to discover how to use them and how to extend API Platform to fit your needs. API Platform is incredibly efficient for prototyping and Rapid Application Development (RAD). But the framework is also designed to create complex web APIs far beyond simple CRUD apps. It benefits from strong extension points and is is continuously optimized for performance. It powers high-traffic websites.
API Platform has a builtin HTTP cache invalidation system which allows to make API Platform apps blazing fast, and it uses Varnish by default. Read more in the chapter API Platform Core Library: Enabling the Builtin HTTP Cache Invalidation System.
API Platform can also be extended using PHP libraries and Symfony bundles.
Here is a non-exhaustive list of popular API Platform extensions:
Keep in mind that you can use your favorite client-side technology: API Platform provides React components ; but you can use your preferred client-side technology including Angular, Ionic and Swift. Any language able to send HTTP requests is OK (even COBOL can do that).
To go further, the API Platform team maintains a demo application showing more advanced use cases like leveraging serialization groups, user management or JWT and OAuth authentication. Checkout the demo code source on GitHub and browse it online.
Made with love by
Les-Tilleuls.coop can help you design and develop your APIs and web projects, and train your teams in API Platform, Symfony, Next.js, Kubernetes and a wide range of other technologies.
Learn more