Creating your First API with API Platform, in 5 Minutes

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:

  1. Install API Platform
  2. Handcraft the API data model as Plain Old PHP Objects

API Platform uses these model classes to expose a web API having a ton of built-in features:

  • creating, retrieving, updating and deleting (CRUD) resources
  • data validation
  • pagination
  • filtering
  • sorting
  • a nice UI and machine-readable documentation (Swagger/OpenAPI, Hydra)
  • hypermedia/HATEOAS and content negotiation support (JSON-LD, HAL)
  • authentication (Basic HTTP, cookies as well as JWT and OAuth through extensions)
  • CORS headers
  • security (tested against OWASP recommendations)
  • HTTP caching
  • and basically everything needed to build modern APIs.

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 very straightforward.

Installing the framework

In Docker container

API Platform is shipped with a Docker setup that makes it easy to get a containerized development environment up and running. This setup contains an image pre-configured with PHP 7, Apache and everything needed to run API Platform and a MySQL image to host the database.

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 Apache and MySQL 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 web bin/console doctrine:schema:create

The web container is where your project stands. Prefixing a command by docker-compose exec web 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.

Via Composer

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
$ php bin/console doctrine:database:create
$ php bin/console doctrine:schema:create

And start the server:

$ php bin/console server:run

It’s ready!

Open http://localhost with your favorite web browser:

Swagger UI integration in API Platform

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. Open http://localhost/foos:

Request detail in the UI

If you want to access the raw data, you have two alternatives:

  • Add the correct Accept header (or don’t set any Accept header at all and API Platform will default to JSON-LD) - preferred when writing API clients
  • Add the format you want as the extension of the resource - for debug purpose only

For 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 very good team collaboration features.

Creating the model

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 number, 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 number if 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 web 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 web 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/Entity.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 notice that some keys start by 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 very 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. An 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:

  1. Put it in the Entity directory of a bundle
  2. If you use Doctrine, map it with the database
  3. Mark it with the @ApiPlatform\Core\Annotation\ApiResource annotation

How can it be more easy?!

Validating Data

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 doesn’t use a valid input, and it’s a very 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 get 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 number if 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 number isn’t valid…

Here we are! We have created a working and very powerful hypermedia REST API in a few minutes, and by writing only a few lines of PHP. But we only covered the basics.

Other features

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 very high-traffic websites.

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 is tested and approved with React, Angular 1 & 2, Ionic and Swift but can work with any language able to send HTTP requests (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.

Next chapter: Testing And Specifying the API

 Edit on GitHub