Upload
spring-io
View
788
Download
1
Embed Size (px)
DESCRIPTION
Speaker: Chris Latimer Essential Grails Track Grails provides rapid API capabilities out of the box, but the developing an API that is ready for public consumption takes a little work.
Citation preview
© 2014 SpringOne 2GX. All rights reserved. Do not distribute without permission.
Building Awesome APIs in GrailsBy Chris Latimer
2
What makes an API awesome?
3
Is it using JSON payloads
instead of XML?
Using this Template
4
Is it strict adherence to REST principles?
5
API Fielding Score
6
7Predictable and Consistent
8
"uri": "/categories/activism", "name": "Activism & Non Profits", "link": “https://vimeo.com/…”, … "metadata": { "connections": {…} }
Category Response:
"uri": "/channels/804185", "name": "School Intercom", "link": “https://vimeo.com/…”, … "metadata": { "connections": {…} }
Channel Response:
9
<photo id="2636" owner="47058503995@N01" secret="a123456" server=“2" title=“test_04” ispublic=“1" isfriend="0" isfamily="0" />
<contact nsid="12037949629@N01" username="Eric" iconserver="1" realname="Eric Costello" friend="1" family="0" ignored="1" />
10
Stable Versions
/v1/endpoint
/v2/endpoint
Accept-‐Version: 1.0
Accept-‐Version: 1.1
URI Based Accept Header
Accept: application/vnd.your.api.v2+json
Accept: application/vnd.your.api.v2.1+json
Content Type
11
Predictable Response Codes
400 Bad Request 401 Unauthorized 403 Forbidden 404 Not Found
2xx Successful 4xx Client Error
500 Server Error 502 Bad Gateway 503 Unavailable
5xx Server Error
200 Success 201 Created
12Intuitive Structure
13
URI Description
/group/{id} A Facebook group
/group/{id}/feed This group’s feed
/group/{id}/files Files uploaded to this group
/group/{id}/events This group’s events
Intuitive URI Structure
14
Intuitive Navigation
"total": 659212, "page": 2, "per_page": 10, "paging": { "next": "/channels?page=3", "previous": "/channels?page=1", "first": "/channels?page=1", "last": "/channels?page=65922" }
Pagination
15
Intuitive Navigation
{ “uri": "/categories/experimental", "name": "Experimental", "subcategories": [ { "uri": “/categories/experimental/animation", "name": "Animation", "link": “https://vimeo.com/categories/…” }… ] }
Related Resources
Flexible Responses
17
Partial Responses
/feeds/api/users/default/uploads
Get Full Response
/feeds/api/users/default/uploads? \ fields=entry(title,gd:comments,yt:statistics)
Get Partial Response
18
Result Filtering
/feeds/api/videos?q=surfing&max-‐results=10
Get List of Videos
/feeds/api/videos?q=surfing&max-‐results=10 &fields=entry[yt:statistics/@viewCount > 1000000]
Get Videos with 1,000,000+ Views
19
Customized Responses
ItemId=B00008OE6I
ItemLookup - Default
ItemId=B00008OE6I &ResponseGroup=Reviews
ItemLookup - Default With Reviews
ItemId=B00008OE6I &ResponseGroup=Large,Reviews,Offers
ItemLookup - Large With Reviews and Offers
20
Easy to Learn and Experiment With
21
22
23
Designing an awesome API
Apps API
G
Phone Shopping App
/phones
/devices
/manufacturers
Potential Resources
Phone Shopping App
Pagination
Filtering
Versioning
API Features
Phone Shopping App
/phoneVariations
/phone/{id}
/phone/{id}/variations
Potential Resources
Phone Shopping App
Complete vs. Compact representations
Including related entities
Linking to other resources
API Features
URI Verb Description
/phones GET Get a list of phones
/phones/{id} GET Get phone details
/phones/{id}/manufacturer GET Get phone’s manufacturer
/phones/{id}/variations GET Get variations of a phone
Phone API Endpoints
Phone API Versioning
Accept-‐Version: 1.0
Accept-‐Version: 2.0
Header Based
{ “entity” : { “attr1” : “value1”, “attr2” : “value2”, … }, “metadata” : { } }
{ “attr1” : “value1”, “attr2” : “value2”, … }
General Response Structure Patterns
{ “entities” : [ { “attr1” : “value1”, “attr2” : “value2”, … },{…},{…}… ], “metadata” : { } }
[ { “attr1” : “value1”, “attr2” : “value2”, … }, {…},{…}… ]
General Response Structure Patterns
API Formats
“phones” : [ { “name”:“iPhone 5s”, “basePrice”:599.99, … } ]
JSON Default<phones> <phone> <name>iPhone 5s</name> … </phone> … </phones>
XML by Request
Intuitive Response Structures
{ “phones” : [ {…}, {…}, …], “paging” : { … } }
Collection ResponseSingle Response{ phone: { “key1” : “value1”, … “keyN” : “valueN”, “links” : [ {…},{…}…] } }
Complete and Compact Representations
Attribute Compact Complete
name
basePrice
variations
manufacturer
Customized Responses
{ “name” : “iPhone5s”, … }
GET /phones/1
{ “name” : “iPhone5s”, … “includes” : [ { “accessories” : […] } ] }
GET /phones/1?include=accessories
PaginationGET /phones{ “phones” : [ {…}, {…}, …], “paging” : { “totalCount” : 233, “currentMax” : 10, “currentOffset” : 40 } }
GET /phones/1{ …, “links” : [ { “rel” : “self”, “href” : “http://…”, }, { “rel” : “manufacturer”, “href” : “http://…” } ] }
Linking to Related Entities
Building an awesome API using
Phone
Manufacturer
Variation
Domain Model
API Features
• Accept Header Versioning
• Predictable Response Codes
• JSON and XML
• RESTful URIs
• Multiple Representations
• Pagination
• Custom Responses
• Related Links
Out of the Box APIs
@Resource(uri="/phones", formats=["json", "xml"]) class Phone { … }
URL Mapping Verb Action
/phones GET List of phones
/phones POST Create new phone
/phones/{id} GET Get single phone
/phones/{id} PUT Update phone
/phones/{id} DELETE Delete phone
@Resource URL Mappings
Read-Only URL Mappings
@Resource(uri="/phones", formats=["json", "xml"], readOnly=true)
URL Mapping Verb Action
/phones GET List of phones
/phones/{id} GET Get single phone
Link is on Twitter - @chrislatimer
Demo
> git clone https://github.com/chrislatimer/gmobile.git
> cd gmobile
> git checkout api-‐step-‐1
API Features
• Accept Header Versioning
• Multiple Representations
• Pagination
• Custom Responses
• Related Links
• Predictable Response Codes
• JSON and XML
• RESTful URIs
Implementing API Versioning
Version 1Controller
Version 2Controller
URL MappingsAPI Requests
Controller Namespaces
class PhoneController extends RestfulController<Phone> { static namespace = 'v1' }
Version 1 Controller
Version 2 Controllerclass PhoneController extends RestfulController<Phone> { static namespace = 'v2' }
URL Mappings"/phones"(version:'1.0', resources:"phone", namespace:'v1')
"/phones"(version:'2.0', resources:"phone", namespace:'v2')
Version 1Controller
Version 2Controller
Accept-‐Version: 1.0
Accept-‐Version: 2.0
Demo
> git checkout api-‐step-‐2
API Features
• Multiple Representations
• Pagination
• Custom Responses
• Related Links
• Predictable Response Codes
• JSON and XML
• RESTful URIs
• Accept Header Versioning
Peanut - Ugliest Dog 2014
"class": "org.gmobile.Phone", "id": 1, "description": null, "manufacturer": { "class": "org.gmobile.Manufacturer", "id": 1 }, "name": "Xtreme Photon Z5", "variations": [ { "class": "org.gmobile.Variation", "id": 1 },… ]
Ugliest JSON 2014?
API Features
• Multiple Representations
• Pagination
• Custom Responses
• Related Links
• Prettier JSON
• Predictable Response Codes
• JSON and XML
• RESTful URIs
• Accept Header Versioning
How can we improve our response payload?
Out of the Box Customization
beans { bookRenderer(XmlRenderer, Book) { includes = [‘title’] } }
include attributes exclude attributes
This can be useful for verysimple customization
beans { bookRenderer(XmlRenderer, Book) { excludes = [‘title’] } }
Simplified Picture of the Response Flow
Controller Renderer Converter Marshaller
as JSON/XML
respond
render
valuemarshal
class PhoneMarshallerJson extends ClosureObjectMarshaller<JSON> {
static final marshal = { Phone phone -‐> def map = [:] … map }
public PhoneMarshallerJson() { super(Phone, marshal) } }
Creating a Custom Marshaller
class PhoneMarshallerXml extends ClosureObjectMarshaller<XML>{
def static final marshal = { Phone phone, XML xml -‐> xml.build { name(phone.name) } }
public PhoneMarshallerXml() { super(Phone, marshal) } }
Creating a Custom Marshaller
beans = { customPhoneJsonMarshaller(ObjectMarshallerRegisterer) { marshaller = new PhoneMarshallerJson() converterClass = JSON priority = 1 }
customPhoneXmlMarshaller(ObjectMarshallerRegisterer) { marshaller = new PhoneMarshallerXml() converterClass = XML priority = 1 } }
Registering a Custom Marshaller
Demo
> git checkout api-‐step-‐3
API Features
• Multiple Representations
• Pagination
• Custom Responses
• Related Links
• Predictable Response Codes
• JSON and XML
• RESTful URIs
• Accept Header Versioning
• Prettier JSON
class PhoneMarshallerJsonCompact extends ClosureObjectMarshaller<JSON>{
public static final marshal = { Phone phone -‐> def map = [:] map.id = phone.id map.name = phone.name map }
public PhoneMarshallerJsonCompact() { super(Phone, marshal) }
}
Creating Named Marshallers
def init = { JSON.createNamedConfig('compact') { it.registerObjectMarshaller(Phone, PhoneMarshallerJsonCompact.marshal) }
JSON.createNamedConfig('complete') { it.registerObjectMarshaller(Phone, PhoneMarshallerJson.marshal) } }
Registering Named Marshallers
Demo
> git checkout api-‐step-‐4
API Features
• Pagination
• Custom Responses
• Related Links
• Predictable Response Codes
• JSON and XML
• RESTful URIs
• Accept Header Versioning
• Prettier JSON
• Multiple Representations
class ApiJsonRenderer<T> extends AbstractRenderer<T> {
public ApiJsonRenderer(Class<T> targetClass) { super(targetClass, MimeType.JSON); }
@Override void render(T object, RenderContext context) { // rendering logic } }
Creating a Custom Renderer
def show(Phone phone) { def detail = params.detail ?: "complete" withFormat { json { respond(phone, [detail:detail]) } xml { XML.use(params?.detail?.toLowerCase() ?: "complete") { respond phone } } } }
Using a Custom Renderer
beans = { phoneRenderer(ApiJsonRenderer, Phone) }
Registering a Custom Renderer
Demo
> git checkout api-‐step-‐5
The monkey wrench in grails.converters.JSON
class ApiJSON extends JSON { … public void renderPartial(JSONWriter out) { initWriter(out) super.value(target) } protected initWriter(JSONWriter out) { writer = out referenceStack = new Stack<Object>(); } }
Creating a Custom Converter
Demo
> git checkout api-‐step-‐6
def index() { … withFormat { json { respond Phone.list(params), [detail:detail, paging:[totalCount: Phone.count(), currentMax: params.max, curentOffset:offset]] } …
Rendering Paging Info
void render(T object, RenderContext context) { … if(context.arguments?.paging) { writer.key("paging") converter = context.arguments.paging as ApiJSON converter.renderPartial(writer) } … }
Rendering Paging Info
Demo
> git checkout api-‐step-‐7
API Features
• Custom Responses
• Related Links
• Predictable Response Codes
• JSON and XML
• RESTful URIs
• Accept Header Versioning
• Prettier JSON
• Multiple Representations
• Pagination
def show(Phone phone) { … withFormat { json { respond(phone, [detail:detail, include:params?.list('include')]) } … } }
Customizing Responses
void render(T object, RenderContext context) { … if(context.arguments?.include) { writer.key("include") writer.array() context.arguments?.include.each { includeProp -‐> JSON.use("compact") { converter = object.properties.get(includeProp) as ApiJSON } writer.object() writer.key(includeProp) converter.renderPartial(writer) writer.endObject() } writer.endArray() } … }
Rendering Custom Responses
Demo
> git checkout api-‐step-‐8
API Features
• Related Links• Predictable Response Codes
• JSON and XML
• RESTful URIs
• Accept Header Versioning
• Prettier JSON
• Multiple Representations
• Pagination
• Custom Responses
static final Closure marshal = { LinkGenerator linkGenerator, Phone phone -‐> def json = [:] json.id = phone.id json.name = phone.name json.links = [] json.links << [rel:"self", href:linkGenerator.link(resource: phone, method: HttpMethod.GET, absolute: true)] json }
Including Related Links
static final Closure marshal = { LinkGenerator linkGenerator, Phone phone -‐> def json = [:] json.id = phone.id json.name = phone.name json.links = [] json.links << [rel:"self", href:linkGenerator.link(resource: phone, method: HttpMethod.GET, absolute: true)] json }
Including Related Links
closure.curry(linkGenerator)
Including Related Links
Demo
> git checkout api-‐step-‐9
API Features
• Predictable Response Codes
• JSON and XML
• RESTful URIs
• Accept Header Versioning
• Prettier JSON
• Multiple Representations
• Pagination
• Custom Responses
• Related Links
Is this API Awesome?
It’s getting there…