Building Next-Gen Web APIs
with JSON-LD and Hydra
Markus Lanthaler
Why do we
need a website?
Of course we
have a website
Why do we
need an API?
1995 2000 2005 2010
Of course we
have an API
Adapted from T. Vitvar’s and J. Musser’s ECOWS 2010 Keynote,
“ProgrammableWeb.com:Statistics, Trends, and Best Practices”
Using Web APIs is still challenging
Level 0:The Swamp of POX
Level 1: Resources
Level 2: HTTPVerbs
{
"id": "cso29ax",
"title": "Symfony Live Portland 2013",
"description": "Prepare slides",
"is_open": true,
"created_at": "2012-11-26T04:49:44Z"
}
http://example.com/issues/cso29ax
BILLION DOLLAR
QUESTION
the
{
"id": "cso29ax",
"title": "Symfony Live Portland 2013",
"description": "Prepare slides",
"is_open": true,
"created_at": "2012-11-26T04:49:44Z"
}
http://example.com/issues/cso29ax
http://example.com/issue/{id}/comments/
{
"id": "cso29ax",
"title": "Symfony Live Portland 2013",
"description": "Prepare slides",
"is_open": true,
"created_at": "2012-11-26T04:49:44Z",
"comments": "/issues/cso29ax/comments/"
}
http://example.com/issues/cso29ax
Level 0:The Swamp of POX
Level 1: Resources
Level 2: HTTPVerbs
Level 3: Hypermedia Controls
{
"id": "cso29ax",
"title": "Symfony Live Portland 2013",
"description": "Prepare slides",
"is_open": true,
"created_at": "2012-11-26T04:49:44Z",
"comments": "/issues/cso29ax/comments/"
}
http://example.com/issues/cso29ax
Every API is a snowflake
Result: tightly coupled & brittle systems
{
"id": "cso29ax",
"title": "Symfony Live Portland 2013",
"description": "Prepare slides",
"is_open": true,
"created_at": "2012-11-26T04:49:44Z",
"comments": "/issues/cso29ax/comments/"
}
http://example.com/issues/cso29ax
{
69 64: 63 73 6f 32 39 61 78
74 69 74 6c 65: 53 79 6d 66 6f 6e 79 20 4c 69 76 …
64 65 73 63 72 69 70 74 69 6f 6e: 50 72 65 70 61 …
69 73 5f 6f 70 65 6e: 01
63 72 65 61 74 65 64 5f 61 74: 32 30 31 32 2d 31 …
63 6f 6d 6d 65 6e 74 73: 2f 69 73 73 75 65 73 2f …
}
http://example.com/issues/cso29ax
Identifiers on the Web: URIs
Linked Data Principles
Tim Berners-Lee, 2006
JSON-LD
Make data self-descriptive by
mapping concepts to URLs
{
"id": "markus",
"firstname": "Markus",
"lastname": "Lanthaler",
"homepage": "http://www.markus-lanthaler.com/"
}
{
"@context": {
"firstname": "http://schema.org/givenName",
"lastname": "http://schema.org/familyName",
"homepage": "http://schema.org/url"
},
"id": "markus",
"firstname": "Markus",
"lastname": "Lanthaler",
"homepage": "http://www.markus-lanthaler.com/"
}
{
"@context": {
"firstname": "http://schema.org/givenName",
"lastname": "http://schema.org/familyName",
"homepage": "http://schema.org/url"
},
"@id": "/people/markus",
"firstname": "Markus",
"lastname": "Lanthaler",
"homepage": "http://www.markus-lanthaler.com/"
}
{
"@context": {
"firstname": "http://schema.org/givenName",
"lastname": "http://schema.org/familyName",
"homepage": "http://schema.org/url"
},
"@id": "/people/markus",
"firstname": "Markus",
"lastname": "Lanthaler",
"homepage": "http://www.markus-lanthaler.com/"
}
{
"@context": {
"firstname": "http://schema.org/givenName",
"lastname": "http://schema.org/familyName",
"homepage": "http://schema.org/url"
},
"@id": "/people/markus",
"firstname": "Markus",
"lastname": "Lanthaler",
"homepage": { "@id": "http://www.markus-lanthaler.com/" }
}
{
"@context": {
"firstname": "http://schema.org/givenName",
"lastname": "http://schema.org/familyName",
"homepage": {
"@id": "http://schema.org/url", "@type": "@id" },
},
"@id": "/people/markus",
"firstname": "Markus",
"lastname": "Lanthaler",
"homepage": "http://www.markus-lanthaler.com/"
}
{
"@context": {
"firstname": "http://schema.org/givenName",
"lastname": "http://schema.org/familyName",
"homepage": {
"@id": "http://schema.org/url", "@type": "@id" }
},
"@id": "/people/markus",
"@type": "http://schema.org/Person",
"firstname": "Markus",
"lastname": "Lanthaler",
"homepage": "http://www.markus-lanthaler.com/"
}
{
"@context": {
"firstname": "http://schema.org/givenName",
"lastname": "http://schema.org/familyName",
"homepage": {
"@id": "http://schema.org/url", "@type": "@id" }
},
"@id": "/people/markus",
"@type": "http://schema.org/Person",
"firstname": "Markus",
"lastname": "Lanthaler",
"homepage": "http://www.markus-lanthaler.com/"
}
{
"@context": "/contexts/person.jsonld",
"@id": "/people/markus",
"@type": "http://schema.org/Person",
"firstname": "Markus",
"lastname": "Lanthaler",
"homepage": "http://www.markus-lanthaler.com/"
}
CMF
{
"id": "cso29ax",
"title": "Symfony Live Portland 2013",
"description": "Prepare slides",
"is_open": true,
"created_at": "2012-11-26T04:49:44Z",
"comments": "/issues/cso29ax/comments/"
}
http://example.com/issues/cso29ax
{
"@context": "/ctx/context.jsonld",
"id": "cso29ax",
"title": "Symfony Live Portland 2013",
"description": "Prepare slides",
"is_open": true,
"created_at": "2012-11-26T04:49:44Z",
"comments": "/issues/cso29ax/comments/"
}
http://example.com/issues/cso29ax
{
"@context": "/ctx/context.jsonld",
"id": "cso29ax",
"title": "Symfony Live Portland 2013",
"description": "Prepare slides",
"is_open": true,
"created_at": "2012-11-26T04:49:44Z",
"comments": "/issues/cso29ax/comments/"
}
http://example.com/issues/cso29ax
{
"@id": "#comments",
"@type": "hydra:Link",
"supportedOperations": [
{
"@id": "#create-comment",
"@type": "hydra:CreateResourceOperation",
"label": "Creates a new comment",
"method": "POST",
"expects": "#Comment",
"returns": "#Comment"
}
]
}
http://example.com/api/doc
{
"@id": "#Comment",
"@type": "hydra:Class",
"supportedProperties": [
{
"property": "#text",
"required": true,
"readonly": false,
"writeonly": false
}
]
}
http://example.com/api/doc
/**
* An Issue tracked by the system.
*
* @HydraExpose()
*/
class Issue
{
/**
* The comments associated with this issue
*
* @HydraExpose()
* @HydraCollection("issue_comments")
* @HydraOperations("issue_comment_create")
*/
private $comments;
// ... other members and methods ...
}
$ php app/console hydra:generate:crud
--entity=MLDemoBundle:Issue
--route-prefix=/issues/
--with-write
--no-interaction
CRUD generation
Generating the CRUD code: OK
You can now start using the generated code!
/**
* Issue controller
*
* @Route("/issues")
*/
class IssueController extends HydraController
{
/**
* Creates a new Issue
*
* @Route("/", name="issue_create")
* @Method("POST")
*
* @HydraOperation(expect = "MLDemoBundleEntityIssue")
*
* @return MLDemoBundleEntityIssue
*/
public function collectionPostAction(Request $request)
{
...
Hydra Console
© 2013, Markus Lanthaler. Some Rights Reserved.
http://creativecommons.org/licenses/by-nc-sa/3.0/
Thank You
Questions?
Markus Lanthaler
http://www.markus-lanthaler.com
@MarkusLanthaler
mail@markus-lanthaler.com
Image Credits
(1) http://www.flickr.com/photos/justinwkern/3729649672/
(2) http://www.flickr.com/photos/alexdram/3095419858/
(3) http://www.flickr.com/photos/kaptainkobold/3203311346/
(11) http://info.cern.ch/hypertext/WWW/TheProject.html
(15) Adapted from http://www.flickr.com/photos/nebarnix/361650027/
(16) http://www.flickr.com/photos/joyoflife/1570126182/
(19) http://www.flickr.com/photos/rossiprojects/5592552858/
(21) http://www.flickr.com/photos/rossiprojects/5592552858/
(23) http://www.flickr.com/photos/clevercupcakes/4397152402/
(31) http://schema.org/Person
(36) http://www.vonwong.com/
(42) http://www.flickr.com/photos/jakecaptive/3205277810/
(47) http://www.flickr.com/photos/sis/126152933/

Building Next-Generation Web APIs with JSON-LD and Hydra