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

Hydra: Describing API Classes, Operations & More

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

So, at least on a high level, we understand that each resource will have an @type key and that this page - via the supportedClass and supportedProperty keys - defines what that type means - what properties it has, and a lot of info about each property.

Right now, we only have one API resource, so we only have one entry under supportedClass, right? Surprise! There's another one called Entrypoint! And another one called ConstraintViolation, which defines the resource that's returned when our API has a validation error.

The Entrypoint Resource

Let's talk about this Entrypoint class: it's a pretty beautiful idea. We already know that when we go to /api, we get, sort of, the HTML version of an API "homepage". Whelp, there is also a JSON-LD version of this page! There's a link to see it at the bottom of this page - but let's get to it a different way.

Find your terminal: we can use curl to see what the "homepage" looks like for the JSON-LD format:

curl -X GET 'https://localhost:8000/api' -H "accept: application/ld+json"

In other words: please make a GET request to /api, but advertise that you would like the JSON-LD format back. I'll also pipe that to jq - a utility that makes JSON look pretty - just skip that if you don't have it installed. And... boom!

Say hello to your API's homepage! Because every URL represents a unique resource, even this page is a resource: it's... an "Entrypoint" resource. It has the same @context, @id and @type, with one real "property" called cheeseListing. That property is the IRI of the cheese listing collection resource.

Heck, this is described in our JSON-LD document! The Entrypoint class has one property: cheeseListing with the type hydra:Link - that's interesting. And, it's pretty ugly, but the rdfs:range part is apparently a way to describe that the resource this property refers to is a "collection" that will have a hydra:member property, which will be an array where each item is a CheeseListing type. Woh!

Hello Hydra

So JSON-LD is all about adding more context to your data by specifying that our resources will contain special keys like @context, @id and @type. It's still normal JSON, but if a client understands JSON-LD, it's going to be able to get a lot more information about your API, automatically.

But in API Platform, there is one other thing that you're going to see all the time, and we're already seeing it! Hydra, which is more than just a many-headed water monster from Greek mythology.

Go back to /api/docs.jsonld. In the same way that this points to the xmls external document so that we can reference things like xmls:integer, we're also pointing to an external document called hydra that defines more "types" or "vocab" we can use.

Here's the idea: JSON-LD gave us the system for saying that this piece of data is this type and this piece of data is this other type. Hydra is an extension of JSON-LD that adds new vocab. It says:

Hold on a second. JSON-LD is great and fun and an excellent dinner party guest. But to really allow a client and a server to communicate, we need more shared language! We need a way to define "classes" within my API, the properties of those classes and whether or not each is readable and writeable. Oh, and we also need to be able to communicate the operations that a resource supports: can I make a DELETE request to this resource to remove it? Can I make a PUT request to update it? What data format should I expect back from each operation? And what is the true identity of Batman?

Hydra took the JSON-LD system and added new "terminology" - called "vocab" - that makes it possible to fully define every aspect of your API.

Hydra Versus OpenAPI

At this point, you're almost definitely thinking:

But wait, this seriously sounds like the exact same thing that we got from our OpenAPI JSON doc.

And, um... yea! Change the URL to /api/docs.json. This is the OpenAPI specification. And if we change that to .jsonld, suddenly we have the JSON-LD specification with Hydra.

So why do we have both? First, yes, these two documents basically do the same thing: they describe your API in a machine-readable format. The JSON-LD and Hydra format is a bit more powerful than OpenAPI: it's able to describe a few things that OpenAPI can't. But OpenAPI is more common and has more tools built around it. So, in some cases, having an OpenAPI specification will be useful - like to use Swagger - and other times, the JSON-LD Hydra document will be useful. With API Platform, you get both.

Phew! Ok, enough theory! Let's get back to building and customizing our API.

Leave a comment!

7
Login or Register to join the conversation
Authoritas Avatar
Authoritas Avatar Authoritas | posted 2 years ago

Wow, this is AMAZING! 🎉🥳Can't wait to play with it. I had wondered how some of our Java devs had setup Swagger so easily and now I know. Thanks! Great stuff, as per usual!

1 Reply

Hello,

Don't know why I have this error.

>curl -X GET 'https://127.0.0.1:8000/api' -H "accept: application/ld+json"

curl: (3) URL using bad/illegal format or missing URL
---------------------------------------------------------------------------------------------------------------------------------------------------------------
>php bin/console debug:router

Name Method Scheme Host Path
------------------------------------- -------- -------- ------ -------------------------------------
_twig_error_test ANY ANY ANY /_error/{code}.{_format}
api_entrypoint ANY ANY ANY /api/{index}.{_format}
api_doc ANY ANY ANY /api/docs.{_format}
api_jsonld_context ANY ANY ANY /api/contexts/{shortName}.{_format}
api_cheese_listings_get_collection GET ANY ANY /api/cheese_listings.{_format}
api_cheese_listings_post_collection POST ANY ANY /api/cheese_listings.{_format}
api_cheese_listings_get_item GET ANY ANY /api/cheese_listings/{id}.{_format}
api_cheese_listings_delete_item DELETE ANY ANY /api/cheese_listings/{id}.{_format}
api_cheese_listings_put_item PUT ANY ANY /api/cheese_listings/{id}.{_format}

Can someone do what should I do?

Thank you in advance

Reply

Hey zahariastefan462

That's very odd. Try using double quotes instead of single quotes for your host parameter

1 Reply
Bedoui A. Avatar
Bedoui A. Avatar Bedoui A. | posted 2 years ago

For Windows users who encountred issues with the curl command, you need to add --ssl-no-revoke at the end
So the full command is :
curl -X GET "https://localhost:8000/api" -H "accept: application/ld+json" --ssl-no-revoke

Reply
Tac-Tacelosky Avatar
Tac-Tacelosky Avatar Tac-Tacelosky | posted 2 years ago

Love API-Platform, using it as much as possible now.

I also use the Symfony proxy, to make my domains easier to work with (symfony proxy:domain:add my-project, then use https://my-project.wip). Can you suggest how to call fetch with a proxy (https://127.0.0.1:7080 is the default).

I keep thinking there must be a way to define the proxy, like in curl, but I can't find it. For now, I'm simply using localhost and the port, as you do in the tutorial, but then I have to always start the servers in the right order. Is there a way to set the proxy in the fetch call? Or somewhere else?

Reply

Hiya Tac-Tacelosky!

Always nice to hear from you :). Beyond what the docs talk about for this - https://symfony.com/doc/cur... - I am not sure, I don't personally use the proxy stuff. I would expect that setting the proxy in your OS and "Re-Applying it" if you're using Chrome (like the docs say) would be enough. Do those docs help at all, or no?

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