Building Awesome APIs with Grails

Preview:

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…

Follow up questions?

@chrislatimer

clatimer@apigee.com