Post on 15-Feb-2017
API MANAGEMENT WITH TAFFY AND API BLUEPRINT
KAI KOENIG (@AGENTK)
AGENDA
▸ What is Taffy?
▸ REST in a nutshell
▸ But is it “RESTful ENOUGH”?
▸ Taffy setup and basic flow
▸ Advanced configuration
▸ API Blueprint and other tools
WHAT IS TAFFY?
WHAT IS TAFFY?
TAFFY
▸ TLDR version: Taffy is a framework to build REST-based APIs in CFML
▸ Developed by Adam Tuttle
▸ Currently in version 3.1, incepted around 2011 (MIT license)
▸ Very stable, very powerful
▸ Allows the creation of REST-based APIs in CFML independent of any vendor implementation - works with Lucee and ACF
▸ Can be integrated with other frameworks
WHAT IS TAFFY?
WHY TAFFY?
“But hey, both Lucee and Adobe CF have their own implementation of REST-API handling and it’s SO EASY to use and it has SELF-DOCUMENTATION! That’s great, ey?”
▸ At first glance it might seem like a great idea for getting started. But:
▸ Various issues (created URL structures, JSON handling, clunky syntax)
▸ Significantly lock-in to a vendor’s implementation
▸ Overall developer-unfriendly approaches
WHAT IS TAFFY?
BENEFITS OF TAFFY
▸ This is not a talk about the deficiencies of REST in ACF or Lucee
▸ This is why Taffy is great:
▸ Developer-friendly and easy to get started with
▸ Backwards-compatible to ACF 8
▸ Convention over configuration
▸ Hardly any boilerplate code
REST IN A NUTSHELL
https://www.flickr.com/photos/kikasz/2891113802/
REST IN A NUTSHELL
WHAT IS REST?
▸ REpresentational State Transfer - a method to communicate over a network
▸ Mostly used over HTTP, technically protocol-independent
▸ 2 phases:
▸ Request - noun, verb, MIME type, headers, data
▸ Response - status code, status text, headers, data
REST IN A NUTSHELL
THE REQUEST (I)
▸ Nouns - identifier of the data we want to work with, e.g. /users or /users/232
▸ Main verbs:
▸ POST: creating a new record (unsafe)
▸ PUT: updating existing record (idempotent)
▸ GET: retrieving existing record (safe)
▸ DELETE: deleting record (idempotent)
REST IN A NUTSHELL
THE REQUEST (II)
▸ Less commonly used verbs:
▸ HEAD: similar to GET, but only returns headers (safe)
▸ OPTIONS: used to check which verbs are available (safe)
▸ PATCH: similar to PUT, updating individual properties (idempotent)
▸ MIME type:
▸ Client and API server agree on the data types for exchange, e.g. text/html, application/xml etc
REST IN A NUTSHELL
THE REQUEST (III)
▸ Headers - supply metadata with the request (Accept, Content-Type)
▸ Data - the data sent from the client to the server
▸ Can conceptually be of any format
▸ Most common: JSON
REST IN A NUTSHELL
THE RESPONSE
▸ Status code & Status text:
▸ “200 OK”
▸ “404 Not Found”
▸ “500 Server Error” …
▸ There are a lot more specific status codes, such as 201, 301, 302, 400, 401 etc.
▸ Headers and Data
REST IN A NUTSHELL
WHY DO PEOPLE LIKE AND USE REST?
▸ Works naturally with the HTTP protocol
▸ Other alternatives:
▸ SOAP-XML: very enterprise-y, bloated and complex
▸ XML-RPC: similar to REST, but ignores the power of the verb
RESTFUL ENOUGH?
https://www.flickr.com/photos/kikasz/2891113802/https://xkcd.com/386/
BUT IS IT RESTFUL ENOUGH?
THE REALITY IS MORE COMPLEX
▸ Roy Fielding’s 2000 dissertation about what we today know as REST
▸ Followed up by a series of blog posts from ~2008 about HATEOAS (Hypermedia As The Engine Of Application State)
▸ prescribing additional requirements and behaviours
▸ often considered as ideal, but impractical in a lot of ways
▸ Leads to a lot of discussion what REST is and how one has to use it
BUT IS IT RESTFUL ENOUGH?
MORE MATURE?
▸ Plain JSON
▸ HATEOAS
{ "name" : “iPhone 7" }
{ "name": “iPhone 7", "links": [ { "rel": "self", "href": "http://localhost:8080/product/1" } ] }
BUT IS IT RESTFUL ENOUGH?
HATEOAS{ "content": [ { "price": 499.00, "name": "iPad", "links": [ { "rel": "self", "href": "http://localhost:8080/product/1" } ], "attributes": { "connector": "socket" } }, { "price": 49.00, "name": "Dock", "links": [ { "rel": "self", "href": "http://localhost:8080/product/3" } ], "attributes": { "connector": "plug" } } ], "links": [ { "rel": "product.search", "href": "http://localhost:8080/product/search" } ] }
BUT IS IT RESTFUL ENOUGH?
RICHARDSON MATURITY LEVEL
http://martinfowler.com/articles/richardsonMaturityModel.html
TAFFY SETUP AND BASIC FLOW
https://www.flickr.com/photos/joanna_young/4515399787/
TAFFY SETUP AND BASIC FLOW
INSTALLATION
▸ Download .zip-file from Github or taffy.io
▸ Installation options:
▸ Global mapping /taffy
▸ /taffy physically in webroot
▸ Installation in sub-directory of site (needs application-specific mappings)
TAFFY SETUP AND BASIC FLOW
DRIVEN BY APPLICATION.CFC
▸ Extends taffy.core.api
▸ Needs to call onApplicationStart() and onRequestStart() on the parent CFC
▸ Earlier versions of Taffy had their own Application.cfc events - it was recommended to actually not implement onApplicationStart() etc
▸ That’s gone since Taffy 2.0
component extends="taffy.core.api" { this.name = "TaffyDemo";
function onApplicationStart() { application.productList = [ { "id":1, "name":"iPhone 7" }, { "id":2, "name":"iPhone 7+" }, { "id":3, "name":"iPad Pro 9.7inch" } ];
return super.onApplicationStart();
}
function onRequestStart() { return super.onRequestStart(); }
}
TAFFY SETUP AND BASIC FLOW
CONVENTION OVER CONFIGURATION
▸ Taffy tries to NOT be in your way
▸ Examples:
▸ Default function names for resources are get(), post(), put(), delete()
▸ Looks for resources in a /resources directory
▸ Lots of things are just fine and work out of the box naturally because of sensible conventions
TAFFY SETUP AND BASIC FLOW
MINIMAL LAYOUT
TAFFY SETUP AND BASIC FLOW
RESOURCES (I)
▸ Extend taffy.core.resource
▸ Live in the /resources directory or a /resources mapping
▸ Can be changed - requires dropping the default bean factory
▸ Good practice:
▸ Have a CFC for a “thing” and for a “collection of things” each
▸ My convention: “SomeThing” and “SomeThingCollection”
▸ A resource should at least implement one verb function
TAFFY SETUP AND BASIC FLOW
RESOURCES (II)
▸ taffy_uri defines the public URL of your resource
▸ representationOf() triggers the default serialiser
▸ withStatus() sets the HTTP status of the response (defaults to “200 OK”)
component extends="taffy.core.resource" taffy_uri="/product" { public function get(){
return representationOf(application.productList).withStatus(200); } }
TAFFY SETUP AND BASIC FLOW
RESOURCES (III)
▸ Passing {productId} token into the resource’s URL
▸ Returning 404 if there’s no record found
component extends="taffy.core.resource" taffy_uri="/product/{productId}" { public function get(numeric productId){
for (var product in application.productList) { if (product["id"] == arguments.productId) { return representationOf(product).withStatus(200, "OK"); } }
return noData().withStatus(404, "Not Found"); } }
TAFFY SETUP AND BASIC FLOW
RESOURCES (IV)
▸ If a verb is not implemented in a resource, Taffy will return “405 Not Allowed”
▸ Tokens will automatically become part of the arguments scope
▸ Tokens support regular expressions: /product/{pId:\d{4}} - /product/{pId:[A-Za-z]+}
▸ Query string parameters will ALSO automatically become part of the arguments scope
▸ Use .noData() if the intention is to not return data
▸ Similar to .withStatus(…), it’s possible to chain .withHeaders(…) to the return statement
TAFFY SETUP AND BASIC FLOW
TAFFY DASHBOARD
TAFFY SETUP AND BASIC FLOW
TAFFY DASHBOARD
▸ Dashboard allows to:
▸ execute requests (incl. Query Params, Headers and Basic Auth)
▸ inspect the response
▸ see a limited amount of documentation
▸ Should (usually) be switched off in production
▸ Default: caches API, needs explicit reloads
TAFFY SETUP AND BASIC FLOW
CHANGING DATA - WHERE TO PUT POST?
▸ If the POST method was in Product.cfc - we’d have an issue.
▸ The URL route requires us to use /product/{productId}
▸ POST therefore has to life in ProductCollection.cfc
▸ PUT however (updating existing data) can happily go into Product.cfc
▸ URL matching: /product/search will match before /product/{productId}
TAFFY SETUP AND BASIC FLOW
GOOD PRACTICES FOR PUT AND POST
▸ If you created a record with POST:
▸ return “201 Created” with the created data
▸ potentially return x-inserted-id in the header
▸ If you updated a record with PUT:
▸ return “200 OK” with the updated data
▸ if the record to be updated didn’t exist, create it (see above)
ADVANCED TASKS AND CONFIG
ADVANCED TASKS AND CONFIG
BREAKING AWAY FROM THE CONVENTIONS
▸ Metadata:
▸ Annotate functions to be triggered by a specific verb: taffy_verb=“<VERB>”
▸ Necessary for extended verbs like OPTIONS, HEAD, PATCH. Optional for standard verbs.
▸ There’s more metadata in serialisers (later)
ADVANCED TASKS AND CONFIG
BREAKING AWAY FROM THE CONVENTIONS
▸ Configuration: variables.framework struct in Taffy’s Application.cfc
▸ reloadKey/reloadPassword vs. reloadOnEveryRequest
▸ serializer/deserializer (later)
▸ disableDashboard
▸ jsonp/allowCrossDomain
▸ beanFactory - allows hooking into Coldspring, DI/1 etc…
ADVANCED TASKS AND CONFIG
AUTHENTICATION AND SECURITY
▸ Firstly - what do you want to achieve?
▸ Requiring an API Token?
▸ Fine-grain security?
▸ oAuth
▸ HTTP Basic Auth
▸ SSL
ADVANCED TASKS AND CONFIG
API TOKEN (I)
▸ Best place to start is onTaffyRequest(…)
▸ Return true to continue processing the request
▸ Return a representation object to abort the request
▸ Two common options for passing token:
▸ URI parameters and/or as part of the data
▸ In the header
ADVANCED TASKS AND CONFIG
API TOKEN (II)
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers, methodMetadata, matchedURI) {
if (not structKeyExists(arguments.requestArguments, "apiKey") || !Len(arguments.requestArguments["apiKey"])) { return noData().withStatus(401, "Unauthorized"); }
if (!arrayFind(application.validAPIKeys, arguments.requestArguments["apiKey"])) { return noData().withStatus(403, "Forbidden"); }
return true; }
ADVANCED TASKS AND CONFIG
CUSTOM SERIALISERS (I)
▸ Default serialiser is JSON (CFML-server built-in)
▸ Extend from taffy.core.baseRepresentation
▸ Implement getAsX functions
▸ getAsXML, getAsJSON, getAsYML etc
ADVANCED TASKS AND CONFIG
CUSTOM SERIALISERS (II)
component extends="taffy.core.baseSerializer" { variables.anythingToXML = application.anythingToXML; variables.jsonUtil = application.jsonUtil;
function getAsXML() output="false" taffy_mime="application/xml" taffy_default="false" { return variables.anythingToXML.toXml(variables.data); }
function getAsJSON() output="false" taffy_mime="application/json" taffy_default="true" { return variables.jsonUtil.serialize(variables.data); } }
ADVANCED TASKS AND CONFIG
RATE LIMITS
▸ In a nutshell:
▸ manage access log (api key & time) in DB or key/value storage
▸ onTaffyRequest logs requests and check if limits are exceeded
▸ if not exceeded, process incoming request
▸ if exceeded, return status code “429 Too Many Requests”
ADVANCED TASKS AND CONFIG
API VERSIONING
▸ Multiple schools of thought:
▸ URI vs header
▸ Make sure you version from day 0!
▸ I tend to use /v1/product, /v2/product etc.
▸ Maps nicely on directory structures in /resources
▸ Update version when breaking compatibility
▸ Consider SemVer (Semantic Versioning) if your API is changing a lot
API BLUEPRINT AND OTHER TOOLS
https://www.flickr.com/photos/cardoso/2196726835/
API BLUEPRINT
WHAT IS IT?
▸ Description and specification language for web APIs (http://apiblueprint.org)
▸ From Apiary.io - but can be used independently from that
▸ Language spec: https://github.com/apiaryio/api-blueprint
▸ Offers platform-neutral documentation and additional tooling
▸ Aglio
▸ API-Mock
API BLUEPRINT
DOCUMENTATION WITH AGLIO
▸ Very simple markup language
▸ npm install -g aglio (https://www.npmjs.com/package/aglio)
▸ aglio -i documentation.md -o documentation.html
▸ MD structure:
▸ # Group
▸ ## Resource
▸ ### Action
API BLUEPRINT
MOCK SERVER WITH API-MOCK
▸ Creates a mock server for your API (https://github.com/localmed/api-mock)
▸ npm install -g api-mock
▸ api-mock ./documentation.md
▸ Defaults to port 3000
API BLUEPRINT
PAW
▸ REST API test and management tool on OS X
▸ Recording/Playback/Testing etc
▸ Has API Blueprint support through extensions
FINAL THOUGHTS
https://www.flickr.com/photos/brickset/16099265973/
FINAL THOUGHTS
WHAT DID WE COVER?
▸ Taffy - what is it?
▸ How to setup and the foundations
▸ Discussed some advanced topics in more detail
▸ High-level overview of API Blueprint
FINAL THOUGHTS
GET IN TOUCH
Kai Koenig
Email: kai@ventego-creative.co.nz
Twitter: @AgentK