Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This tutorial has a new version, check it out!

Swagger: Instant, Interactive API Docs

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

We're currently looking at something called Swagger: an open source API documentation interface. We're going talk more about it soon, but the idea is basically this: if you have an API - built in any language - and you create some configuration that describes that API in a format that Swagger understands, boom! Swagger can render this beautiful interactive documentation for you. Behind the scenes, API Platform is already preparing that configuration for Swagger.

Let's play with it! Open the POST endpoint. It says what this does and shows how the JSON should look to use it. Nice! Click "Try it out"! Let's see what's in my kitchen - some "Half-eaten blue cheese", which is still... probably ok to eat. We'll sell it for $1. What a bargain! And... Execute!

Um... what happened? Scroll down. Woh! It just made a POST request to /api/cheese_listings and sent our JSON! Our app responded with a 201 status code and... some weird-looking JSON keys: @context, @id and @type. Then it has the normal data for the new cheese listing: the auto-increment id, title, etc. Hey! We already have a working API... and this just proved it!

Close up the POST and open the GET that returns a collection of cheese listings. Try this one out too: Execute! Yep... there's our one listing... but it's not raw JSON. This extra stuff is called JSON-LD. It's just normal JSON, but with special keys - like @context - that have a specific meaning. Understanding JSON-LD is an important part of leveraging API Platform - and we'll talk more about it soon.

Anyways, to make things more interesting - go back to the POST endpoint and create a second cheese listing - a giant block of cheddar cheese... for $10. Execute! Same result: 201 status code and id 2.

Try the collection GET endpoint again. And... alright! Two results, with ids 1 and 2. And if we want to fetch just one cheese listing, we can do that with the other GET endpoint. As you can see, the id of the cheese listing that we want to fetch is part of the URL. This time, when we click to try it, cool! It gives us a box for the id. Use "2" and... Execute!

This makes a very simple GET request to /api/cheese_listings/2, which returns a 200 status code and the familiar JSON format.

Content-Type Negotiation

How cool is this! A full API "CRUD" with no work. Of course, the trick will be to customize this to our exact needs. But sheesh! This is an awesome start.

Let's try to hit our API directly - outside of Swagger - just to make sure this isn't all an elaborate trick. Copy the URL, open a new tab, paste and... hello JSON! Woh! Hello... API doc page again?

It scrolled us down to the documentation for this endpoint and executed it with id 2... which is cool... but what's going on? Do we actually have a working API or not?

Built into API Platform is something called Content-Type negotiation. Conveniently, when you execute an operation, Swagger shows you how you could make that same request using curl at the command line. And it includes one critical piece:

-H "accept: application/ld+json"

That says: make a request with an Accept header set to application/ld+json. The request is hinting to API Platform that it should return the data in this JSON-LD format. Whether you realize it or not, your browser also sends this header: as text/html... cause... it's a browser. That basically tells API Platform:

Hey! I want the CheeseListing with id 2 in HTML format.

API Platform responds by doing its best to do exactly that: it returns the HTML Swagger page with CheeseListing id 2 already showing.

Faking the Content-Type

This isn't a problem for an API client because setting an Accept header is easy. But... is there some way to kinda... "test" the endpoint in a browser? Totally! You can cheat: add .jsonld to the end of the URL.

Boom! This is our API endpoint in the JSON-LD format. I called this "cheating" because this little "trick" of adding the extension is really only meant for development. In the real world, you should set the Accept header instead, like if you were making an AJAX request from JavaScript.

And, check this out: change the extension to .json. That looks a bit more familiar!

This is a great example of the API Platform philosophy: instead of thinking about routes, controllers and responses, API Platform wants you to think about creating API resources - like CheeseListing - and then exposing that resource in a variety of different formats, like JSON-LD, normal JSON, XML or exposing it through a GraphQL interface.

Where do These Routes Come From?

Of course, as awesome as that is, if you're like me, you're probably thinking:

This is cool... but how did all these endpoints get magically added to my app?

After all, we don't normally add an annotation to an entity... and suddenly get a bunch of functional pages!

Find your terminal and run:

php bin/console debug:router

Cool! API platform is bringing in several new routes: api_entrypoint is sort of the "homepage" of our api, which, by the way, can be returned as HTML - like we've been seeing - or as JSON-LD, for a machine-readable "index" of what's included in our API. More on that later. There's also a /api/docs URL - which, for HTML is the same as going to /api, another called /api/context - more on that in a minute - and below, 5 routes for the 5 new endpoints. When we add more resources later, we'll see more routes.

When we installed the API Platform pack, its recipe added a config/routes/api_platform.yaml file.

api_platform:
resource: .
type: api_platform
prefix: /api

This is how API Platform magically adds the routes. It's not very interesting, but see that type: api_platform? That basically says:

Hey! I want API Platform to be able to dynamically add whatever routes it wants.

It does that by finding all the classes marked with @ApiResource - just one right now - creating 5 new routes for the 5 operations, and prefixing all the URLs with /api. If you want your API URLs to live at the root of the domain, just change this to prefix: /.

I hope you're already excited, but there is so much more going on than meets the eye! Next, let's talk about the OpenAPI spec: an industry-standard API description format that gives your API Swagger superpowers... for free. Yes, we need to talk a little bit of theory - but you will not regret it.

Leave a comment!

23
Login or Register to join the conversation
Covi A. Avatar
Covi A. Avatar Covi A. | posted 2 years ago

i have appearance problem. can you please tell me what the actual problem
when your page show like this
your browser

my browser show like this
my browser

i am something confused. please clear me.

Reply

Hey Monoranjan,

Oh, haha, it's a tircky Google Chrome plugin that highlights JSON reponses :) It's called JSONView, you can install this extension for *your* browser and your JSON responses will look exactly like Ryan's ;) Btw, we mentioned this earlier in this course, or in previous courses, but I agree, it's easy to miss :) But now you know! Happy coding with nice JSON highlights now ;)

Cheers!

Reply
Covi A. Avatar

thank you very much for your answer. now it's working successfully

Reply
Thijs-jan V. Avatar
Thijs-jan V. Avatar Thijs-jan V. | posted 2 years ago

Hi guys,

Hopefully you can give me a suggestion for my issue: I would like to start using a REST api in my existing project, however the problem is that it contains 300+ different entity types. For API-platform it seems not to be a problem, however the Swagger UI interface crashes my browser, just because of all not-yet-filled schemas (and endpoints for 2 entities as test). Do you a way to split the API in sections, documented on separate pages? Or is the project simply too big for Swagger UI?

Reply

Hey Thijs-jan V.,

Wow this looks like a BIG project, I'm not sure about how many entities can process default swagger connection, but there is a way to totally customise how it works. Probably it a little complex but it can be a good way. You can read about swagger here https://api-platform.com/do... I like the section about "Overriding the OpenAPI Specification"

PS BTW another tip is to use totally custom swagger it's not too difficult, but very cool to play =)

Cheers!

Reply
Daniel W. Avatar
Daniel W. Avatar Daniel W. | posted 2 years ago | edited

Hi,

at 8:08 you showed how to change the prefix.
Is it possible to change the prefix for the whole symfony app? so every single route even stuff like "/_profiler/ "?

I'm running multiple symfony applications parallel and I want each of them to have a distinct prefix so otherwise some features like the profile don't work.

For example I want that "localhost/users" is the true root of app1 not just localhost
and "localhost/cheeselisting" be the root of app2. So the path for the profile would be "localhost/user/_profiler" for the user app.

I have an ingress that decides where to route the request based on the path.
for example:
/users/* -> user server == every request that starts with /users will be passed the the user server<br /> /cheeses/* -> user server == every request that starts with /cheeses will be passed the the cheeses server

I hope I could explain it understandable ^^

Reply

Hey Ecludio,

Check the Kernel::configureRoutes() in your src/Kernel.php. That method imports all the routes into the system, that import() call allows you to set a prefix via 2nd argument, by default it's "/". I believe that's exactly what you need :)

Cheers!

Reply
Apr Avatar
Apr Avatar Apr | posted 2 years ago | edited

Hi!

Why is my api platform documentation empty?

When i go to the POST option, the body documentation is empty. After i click 'try it out' i only see an empty JSON {}. I had to write the JSON by hand following the example in the video like this:
{<br /> "title": "Cheese1",<br /> "description": "description 1",<br /> "price": 1000,<br /> "createdAt": "2021-02-02/22:32:23+00:00Z",<br /> "isPublished": true<br />}

And i get the following error:
<blockquote>"An exception occurred while executing 'INSERT INTO cheese_listing (title, description, price, created_at, is_published) VALUES (?, ?, ?, ?, ?)' with params [null, null, null, null, null]:\n\nSQLSTATE[23000]: Integrity constraint violation: 1048 Column 'title' cannot be null",</blockquote>

Why can't i see the documentation inside the body to send a POST request to create my first item? and why i'm getting 500 status after sending all "null"?

My Entity has the @ApiResource() and the fields are created as in the tutorial. I think i didn't miss any step, the migrations didn't throw any errors either.

Am i missing something?

Thanks!.

Reply

Hey Saul,

Hm, difficult to say, it looks like you missed something important from the tutorial. Did you download the course code and started from the start/ directory? Or are you following this course on your own project? Please, also, double-check the namespaces of the annotation you're using in your entities, probably you forgot the namespace or used incorrect one. I'd recommend you literally expand all the lines in the code block for the entity you're interested in and copy/paste it into your project. Does it help? Or you still don't see anything? Btw, are you in dev Symfony mode? Please, also try to clear the cache just in case! Does it help?

I really hope this helps!

Cheers!

Reply
Kristijan G. Avatar
Kristijan G. Avatar Kristijan G. | posted 2 years ago

Hi,

is there any way to change default scheme in Swagger UI to HTTP instead of HTTPS?

Thanks.

Reply

Hey Gets,

Do you run the project via "symfony serve" command? If so, try "symfony serve --no-tls", or probably better "symfony serve --allow-http" to allow both HTTP and HTTPS connections.

I hope this helps!

Cheers!

Reply
Default user avatar
Default user avatar isTom | posted 2 years ago | edited

Hello,

I have a weird unwanted addition called <b>additionalProp1</b> in all my entities docs Example Value
<br />{<br /> "@context": "string",<br /> "@id": "string",<br /> "@type": "string",<br /> "id": 0,<br /> "title": "string",<br /> "created_at": "2020-12-15T13:20:04.604Z",<br /> "updated_at": "2020-12-15T13:20:04.605Z",<br /> "additionalProp1": {}<br />}<br />

Have no idea where it coming from. In the Schema tab it is not present
<br />content_providers:jsonld-Read{description:ContentProviders@contextstring<br />readOnly: true@idstring<br />readOnly: true@typestring<br />readOnly: trueidinteger<br />readOnly: truetitle*string<br />nullable: truecreated_atstring($date-time)updated_atstring($date-time)}<br />

Reply

Hey @isTom

Does it only appear on your docs or it's also a field of your API endpoints?

Reply
Default user avatar

They only appear on /api/docs. API responses look ok.

Reply

Hey, sorry for my late response. If the API endpoints are ok, then I believe you added that extra information to your docs through any of the methods shown in this page https://api-platform.com/do...

Reply
Default user avatar
Default user avatar isTom | MolloKhan | posted 2 years ago | edited

Hello Diego,
I cannot identify any additional information in my class and sure there is nothing named <b>additionalProp1</b>
`
/**

  • ContentProviders
    *
  • @ORM\Table(name="content_providers",
  • indexes={
  • @ORM\Index(name="title", columns={"title"}),
  • },
  • uniqueConstraints={@ORM\UniqueConstraint(columns={"identification"})}
  • )
  • @UniqueEntity(fields={"identification"}, message="app.unique_value_required")
  • @ApiResource(
  • collectionOperations={"get", "post"},
  • itemOperations={
  • "get"={},
  • "put"
  • },
  • shortName="content_providers",
  • normalizationContext={"groups"={"content_providers:read"}, "swagger_definition_name"="Read"},
  • denormalizationContext={"groups"={"content_providers:write"}, "swagger_definition_name"="Write"},
  • )
  • @ORM\Entity
    */

class ContentProviders
{

/**
 * @var int
 *
 * @ORM\Column(name="id", type="integer", nullable=false, options={"unsigned"=true})
 * @ORM\Id
 * @ORM\GeneratedValue(strategy="IDENTITY")
 * @Groups({"content_providers:read"})
 */
private $id;
/**
 * ContentProviders in the VodSource.
 *
 * @var VodSource
 *
 * @ORM\OneToMany(targetEntity="App\Entity\Main\VodSource", mappedBy="contentProvider")
 * @Groups({"content_providers:read"})
 **/
protected $vodSource;
/**
 * ContentProviders in the VodCreated.
 *
 * @var VodCreated
 *
 * @ORM\OneToMany(targetEntity="App\Entity\Main\VodCreated", mappedBy="contentProvider")
 **/
protected $vodCreated;
/**
 * @var string|null
 *
 * @ORM\Column(name="title", type="string")
 * @Groups({"content_providers:read", "content_providers:write"})
 * @Assert\NotBlank()
 */
private $title;
/**
 * @var string|null
 *
 * @ORM\Column(name="identification", type="string")
 * @Groups({"content_providers:read", "content_providers:write"})
 * @Assert\NotBlank()
 */
private $identification;
/**
 * @var \DateTime|null
 *
 * @ORM\Column(name="created_at", type="datetime", columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP")
 * @Groups({"content_providers:read"})
 */
private $createdAt;
/**
 * @var \DateTime|null
 *
 * @ORM\Column(name="updated_at", type="datetime", columnDefinition="TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP")
 * @Groups({"content_providers:read"})
 */
private $updatedAt;

`

Reply

Ha! That's weird. What if you commit everything and then execute git grep additionalProp1 so you can track down where it's coming from

Cheers!

Reply
Default user avatar

Any way to disable "Try it out" buttons in Swagger UI?

Reply

Hey @Mike!

Hmm, I've never tried it! It looks like it's a 2 step process:

1) You disable it in JavaScript (since Swagger is a Js library) https://api-platform.com/do...

2) How do you get custom JS onto the page? By overriding/extending the template that renders Swagger: https://api-platform.com/do...

Let me know if it works!

Cheers!

Reply
Default user avatar
Default user avatar Kasper Smithsonian | posted 3 years ago | edited

`api_platform:

            resource: .
            type: api_platform
            prefix: /api

`
in api_plataform.yaml not work for me , I have to put in route.yaml of config

Reply

Hey Kasper Smithsonian

I believe you got confused by the file name. This api_platform.yaml files should live inside config/routes not inside config/packages

Cheers!

Reply
ddlzz Avatar

Hi!

Is there any way to redefine swagger operations description (like 'Retrieve foo collection') using an `@ApiResource` configuration?
I tried this
```
* @ApiResource(
* collectionOperations={
* "send": {
* "method": "POST",
* "path": "/email/send",
* "swaggerContext": {
* "summary": "test summary",
* "description": "sends a confirmation code",
* },
* "controller": SendEmailConfirmation::class,
* },
* },
```
but it doesn't work

Reply

Hey ddlzz

I haven't done what you are asking but I think you can do it by following this part of the documentation: https://api-platform.com/do...

I hope it helps. Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial works great for Symfony 5 and API Platform 2.5/2.6.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^2.1", // v2.4.3
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "doctrine/annotations": "^1.0", // 1.10.2
        "doctrine/doctrine-bundle": "^1.6", // 1.11.2
        "doctrine/doctrine-migrations-bundle": "^2.0", // v2.0.0
        "doctrine/orm": "^2.4.5", // v2.7.2
        "nelmio/cors-bundle": "^1.5", // 1.5.5
        "nesbot/carbon": "^2.17", // 2.19.2
        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0", // 4.3.1
        "symfony/asset": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/console": "4.2.*", // v4.2.12
        "symfony/dotenv": "4.2.*", // v4.2.12
        "symfony/expression-language": "4.2.*|4.3.*|4.4.*", // v4.3.11
        "symfony/flex": "^1.1", // v1.17.6
        "symfony/framework-bundle": "4.2.*", // v4.2.12
        "symfony/security-bundle": "4.2.*|4.3.*", // v4.3.3
        "symfony/twig-bundle": "4.2.*|4.3.*", // v4.2.12
        "symfony/validator": "4.2.*|4.3.*", // v4.3.11
        "symfony/yaml": "4.2.*" // v4.2.12
    },
    "require-dev": {
        "symfony/maker-bundle": "^1.11", // v1.11.6
        "symfony/stopwatch": "4.2.*|4.3.*", // v4.2.9
        "symfony/web-profiler-bundle": "4.2.*|4.3.*" // v4.2.9
    }
}
userVoice