GET /cfml
A Guide to Writing API Wrappers
Matthew Clemente
Also Called:
APIs are awesome!
Everything has an API
Everything has an API
- Payments
- Search
- AI
- Natural Language Processing
- Physical Mail
- Maps
- APIs
- E-Commerce
- IoT
- SMS/Voice
- Accounting
- Books
- Calendars
- Cloud Storage
- Validation
- Dictionaries
- Weather
- Ticketing
- Music
- Recipes
- Image Recognition
- Beer
- Star Wars
Don’t Reinvent the Wheel
How do we get started?
Step 1: Don't Write Anything!
How did I get here?
Every API is different
All APIs are the same
and also
REST
REpresentational State Transfer
REST is CRUD over HTTP. Some exceptions to this rule are allowed as long as they don’t become the rule.
Method + Host + Path + Query Params + Body + Headers
Status Code + Status Text + Headers + Data
api-wrapper-template
A CommandBox tool for scaffolding CFML API Clients
-
Quickly create an API wrapper project
-
Scaffold boilerplate HTTP request handling
-
Structured to mirror API documentation
-
Handle various response formats
-
Provide additional information about request/response
box install api-wrapper-template
Don’t Reinvent the Wheel
Getting Started
- Read the documentation
-
Authentication / Authorization
-
Endpoint / Test Endpoint
-
Rate limits
-
Response Format
-
Other limitations (size of request, etc.)
-
- Review other client libraries
The Cat API
The Cat API Wrapper
apiWrapper scaffold --wizard
apiName* | Name of the API this library will wrap. [i.e. Stripe] |
apiEndpointUrl* | Base endpoint URL for API calls. [i.e. https://api.stripe.com/v1] |
apiAuthentication | Type of authentication used [None, Basic, Apikey, Other] |
apiDocUrl | URL of the API documentation homepage |
name | Name for the wrapper [i.e. StripeCFC] |
description | A short description of the wrapper. |
author | Name of the author of the wrapper. |
quickStart | Do you want to quickStart the project? |
The Cat API Wrapper
apiwrapper scaffold apiName="Cat API" apiEndpointUrl="http://thecatapi.com/api" apiAuthentication="none" apiDocUrl="https://thecatapi.com/docs.html" name="CFCat" description="Some people are cat people. Some people are dog people. This wrapper will give the former random cats on demand." && cd catWrapper && server start openbrowser=false
apiName | Cat API |
apiEndpointUrl | http://thecatapi.com/api |
apiAuthentication | none |
apiDocUrl | https://thecatapi.com/docs.html |
name | CFCat |
description | Some people are cat people. Some people are dog people. This wrapper will give the former random cats on demand. |
The Cat API Wrapper
Read the documentation!
Making the Request
Building Your Requests
Making the Request
/**
* http://thecatapi.com/docs.html#images-get
* @hint List cats
*/
public struct function listCats( string category = '' ) {
var params = { 'format' : 'html' };
if ( category.len() ) params[ 'category' ] = category;
return apiCall( 'GET', '/images/get', params );
}
Naming Your Methods
- Be Consistent
- Follow basic CRUD conventions
-
createResource()
-
getResource()
-
listResources()
-
updateResource()
-
deleteResource()
-
- Use language of documentation
- For non-CRUD operations, look to other clients
(Make sure there's a method to your madness)
The Result of our Request
cat = new cat.cat();
catImage = cat.listCats().data;
writeOutput( catImage );
API Responses > Data
cat = new cat.cat();
catImage = cat.listCats();
writeDump( catImage );
Make Debugging Easy
RequestBin / Hookbin / Mockbin
Being able to send your API wrappers requests
to a different endpoint is helpful when debugging.
Make Debugging Easy
cat = new cat.cat( includeRaw = true );
catImage = cat.listCats();
writeDump( catImage );
Authentication
Authentication typically involves providing credentials via Headers
No Authentication | This is easy! You don't need to do anything! Yay! |
Basic Authentication | Base64 encoded username:password in Authorization header |
API Key Authentication | Unique identifiers passed via headers ( or, less frequently, query params or in the body) |
Open Authorization (OAuth) | Access Token used, following authentication and permission |
Authentication
How do we handle it in our CFML API clients?
No Authentication | This is easy! You don't need to do anything! Yay! |
Basic Authentication |
cfhttp
username and
password attributes
|
API Key Authentication |
cfhttpparam( type = "header" ... )
Handled via getBaseHttpHeaders() in the
api-wrapper-template
|
Open Authorization (OAuth) | https://github.com/coldfumonkeh/oauth2 |
Environment Variables
Keeping secrets in environment variables
provides practical and security benefits
// Get access to the system class.
system = createObject( "java", "java.lang.System" );
value = system.getenv( 'key' );
// Calling getenv() without an argument returns a map / struct of all of the
// keys available in the current environment (to the ColdFusion user).
environment = system.getenv();
Environment Variables
Handled automatically in
api-wrapper-template
var secrets = {
'applicationId': 'AYLIEN_APPLICATION_ID',
'applicationKey': 'AYLIEN_APPLICATION_KEY'
};
Aylien Text Analysis API
Aylien Text Analysis API
Aylien Text Analysis API
apiwrapper scaffold apiName="Aylien" apiEndpointUrl="https://api.aylien.com/api/v1" apiAuthentication="apikey" apiDocUrl="http://docs.aylien.com/textapi/#getting-started" name="ayliencfc" description="Access Aylien's Natural Language Processing (NLP) API to extract meaning and insight from textual content." && cd aylienWrapper && server start openbrowser=false
apiName | Aylien |
apiEndpointUrl | https://api.aylien.com/api/v1 |
apiAuthentication | apikey |
apiDocUrl | http://docs.aylien.com/textapi/#getting-started |
name | ayliencfc |
description | Access Aylien's Natural Language Processing (NLP) API to extract meaning and insight from textual content. |
Handling API Keys
public any function init(
string applicationId = '',
string applicationKey = '',
string baseUrl = "https://api.aylien.com/api/v1",
boolean includeRaw = false ) {
Handling API Keys
private struct function getBaseHttpHeaders() {
return {
'Accept' : 'application/json',
'Content-Type' : 'application/json',
'X-AYLIEN-TextAPI-Application-Key' : variables.applicationKey,
'X-AYLIEN-TextAPI-Application-ID' : variables.applicationId,
'User-Agent' : 'ayliencfc/#variables._ayliencfc_version# (ColdFusion)'
};
}
Making a Request
/**
* https://docs.aylien.com/textapi/endpoints/#entity-extraction
* @hint Extracts different types of notable entities from a document
*/
public struct function entities( string text = '', string url = '', string language ) {
var params = {
'language' : language ?: 'auto'
};
if ( text.len() )
params[ 'text' ] = text;
if ( url.len() )
params[ 'url' ] = url;
return apiCall( 'POST', '/entities', params );
}
URL to Analyze
Aylien Entity Analysis
aylien = new aylien.aylien();
entities = aylien.entities( url = 'http://bit.ly/intb2018' ).data;
writeDump( entities );
Aylien Response Headers
aylien = new aylien.aylien();
entities = aylien.entities( url = 'http://bit.ly/intb2018' );
writeDump( entities );
Shouldn't We Validate?
No...
Don’t Reinvent the Wheel
Leave Validation to the API
aylien = new aylien.aylien();
entities = aylien.entities( url = 'I am not a URL!' );
writeDump( entities );
Your Users are Developers
Make their lives easier by identifying pain points.
entities = aylien.entities( 'http://bit.ly/intb2018' ).data;
Developer-focused Refactor #1
Developer-focused Refactor #2
Lob API for Printables
Lob API for Printables
apiwrapper scaffold apiName="Lob" apiEndpointUrl="https://api.lob.com/v1" apiAuthentication="basic" apiDocUrl="https://lob.com/docs#intro" name="lobcfc" description="Wrap the Lob API to verify addresses and send physical mail programmatically." && cd lobWrapper && server start openbrowser=false
apiName | Lob |
apiEndpointUrl | https://api.lob.com/v1 |
apiAuthentication | basic |
apiDocUrl | https://lob.com/docs#intro |
name | lobcfc |
description | Wrap the Lob API to verify addresses and send physical mail programmatically. |
Lob API for Printables
Basic Auth & Test Endpoints
public any function init(
string apiKey = '',
string testApiKey = '',
boolean forceTestMode = false,
string baseUrl = "https://api.lob.com/v1",
boolean includeRaw = false ) {
var secrets = {
'apiKey': 'LOB_LIVE_API_KEY',
'testApiKey': 'LOB_TEST_API_KEY'
};
Basic Auth & Test Endpoints
cfhttp( url = fullPath, method = httpMethod, username = !variables.forceTestMode ? variables.apikey : variables.testApiKey, password = '', result = 'result' ) {
Lob Test Endpoint
lob = new lob.lob( forceTestMode = true );
//etc etc
result = lob.createPostcard( postcard );
Getting Into Arguments
How To Lose Arguments
How To Defuse Arguments
- Easiest to implement
- Commonly used by official clients
- Preferable to listing arguments
- Puts burden on developer
- Data not reusable
(The fast and easy approach)
How To Win Arguments
(Helper components with fluent interfaces)
- More initial work for you
- Significantly better developer experience
- Component reuse ensures consistent data
Helper Component Props
/helpers/address.cfc
/**
* lobcfc
* Copyright 2018 Matthew J. Clemente, John Berquist
* Licensed under MIT (https://mit-license.org)
*/
component accessors="true" {
property name="description" default="";
property name="name" default="";
property name="company" default="";
property name="address_line1" default="";
property name="address_line2" default="";
property name="address_city" default="";
property name="address_state" default="";
property name="address_zip" default="";
property name="address_country" default="";
property name="phone" default="";
property name="email" default="";
property name="metadata" default="";
/**
* https://lob.com/docs#addresses_object
* @hint No parameters can be passed to init this component. They must be built manually. When creating and updating addresses, the following fields are required:
* name or company (both can be provided)
* address_line1
* address_city (if in US)
* address_zip (if in US)
*/
public any function init() {
setMetadata( {} );
return this;
}
/**
* @hint An internal description that identifies this resource.
*/
public any function description( required string description ) {
setDescription( description );
return this;
}
/**
* @hint Either name or company is required
*/
public any function name( required string name ) {
setName( name );
return this;
}
/**
* @hint Either name or company is required
*/
public any function company( required string company ) {
setCompany( company );
return this;
}
/**
* @hint Required
*/
public any function addressLine1( required string address ) {
setAddress_line1( address );
return this;
}
/**
* @hint alias for setting address line 1
*/
public any function address( required string address ) {
return addressLine1( address );
}
public any function addressLine2( required string address ) {
setAddress_line2( address );
return this;
}
/**
* @hint alias for setting address line 2
*/
public any function address2( required string address ) {
return addressLine2( address );
}
/**
* @hint Required if address is in US
*/
public any function city( required string city ) {
setAddress_city( city );
return this;
}
/**
* @hint alias for setting city
*/
public any function addressCity( required string city ) {
return city( city );
}
/**
* @hint Required if address is in US. Can accept either a 2 letter state short-name code or a valid full state name
*/
public any function state( required string state ) {
setAddress_state( state );
return this;
}
/**
* @hint alias for setting state
*/
public any function addressState( required string state ) {
return state( state );
}
/**
* @hint Required if address is in US. Can accept either a ZIP format of 12345 or ZIP+4 format of 12345-1234
*/
public any function zip( required string zip ) {
setAddress_zip( zip );
return this;
}
/**
* @hint alias for setting zip
*/
public any function addressZip( required string zip ) {
return zip( zip );
}
/**
* @hint 2 letter country short-name code (ISO 3166). Defaults to US.
*/
public any function country( required string country ) {
setAddress_country( country );
return this;
}
/**
* @hint alias for setting country
*/
public any function addressCountry( required string country ) {
return country( country );
}
public any function phone( required string phone ) {
setPhone( phone );
return this;
}
public any function email( required string email ) {
setEmail( email );
return this;
}
/**
* @hint Use metadata to store custom information for tagging and labeling back to your internal systems. Must be an object with up to 20 key-value pairs. Keys must at most 40 characters and values must be at most 500 characters. Neither can contain the characters " and \. Nested objects are not supported. See https://lob.com/docs#metadata for more information.
* @metadata if a struct is provided, it will be serialized. Otherwise the string will be set as provided
*/
public any function metadata( required any metadata ) {
if ( isStruct( metadata ) )
setMetadata( serializeJSON( metadata ) );
else
setMetadata( metadata );
return this;
}
/**
* @hint The zip code needs to be handled via a custom method, to force it to be passed as a string
*/
public string function build() {
var body = '';
var properties = getPropertyValues();
var count = properties.len();
properties.each(
function( property, index ) {
var value = property.key != 'address_zip' ? serializeJSON( property.value ) : serializeValuesAsString( property.value );
body &= '"#property.key#": ' & value & '#index NEQ count ? "," : ""#';
}
);
return '{' & body & '}';
}
/**
* @hint helper that forces object value serialization to strings. This is needed in some cases, where CF's loose typing causes problems
*/
private string function serializeValuesAsString( required any data ) {
var result = '';
if ( isStruct( data ) ) {
var serializedData = data.reduce(
function( result, key, value ) {
if ( result.len() ) result &= ',';
return result & '"#key#": "#value#"';
}, ''
);
result = '{' & serializedData & '}';
} else if ( isNumeric( data ) ) {
result = '"#data#"';
} else if ( isArray( data ) ) {
var serializedData = data.reduce(
function( result, item, index ) {
if ( result.len() ) result &= ',';
return result & '"#item#"';
}, ''
);
result = '[' & serializedData & ']';
}
return result;
}
/**
* @hint converts the array of properties to an array of their keys/values, while filtering those that have not been set
*/
private array function getPropertyValues() {
var propertyValues = getProperties().map(
function( item, index ) {
return {
"key" : item.name,
"value" : getPropertyValue( item.name )
};
}
);
return propertyValues.filter(
function( item, index ) {
if ( isStruct( item.value ) )
return !item.value.isEmpty();
else
return item.value.len();
}
);
}
private array function getProperties() {
var metaData = getMetaData( this );
var properties = [];
for( var prop in metaData.properties ) {
properties.append( prop );
}
return properties;
}
private any function getPropertyValue( string key ){
var method = this["get#key#"];
var value = method();
return value;
}
}
Chaining Methods
/helpers/address.cfc
lob = new lob.lob();
address = new lob.helpers.address()
.description( 'Into The Box 2018 - Demo' )
.company( 'Hyatt Place Houston/The Woodlands' )
.name( 'Guest: Matthew Clemente (4/25)' )
.address( '1909 Research Forest Drive' )
.city( 'The Woodlands' ).state( 'Texas' ).zip( '77380' );
result = lob.createAddress( address );
writeDump( var='#result#', format='html', abort='true' );
/**
* https://lob.com/docs#addresses_create
* @hint Create an address
* @address this should be an instance of the `helpers.address` component. However, if you want to create and pass in the struct or json yourself, you can.
*/
public struct function createAddress( required any address ) {
var body = {};
if ( isValid( 'component', address ) )
body = address.build();
else
body = address;
return apiCall( 'POST', '/addresses', {}, body );
}
Something is Wrong Here
Document Your Wrapper
(Because don't you prefer well documented repositories)
- Docs shouldn't be an afterthought
- Add a function, add it to the docs
- Link to official docs
- Provide a quick start guide/example
- Describe the install procedure
- List the functions you provide
- Model a repo you admire
Share Your API Wrappers!
Don’t Reinvent the Wheel
GET /cfml - A Guide to Writing API Wrappers
By mjclemente
GET /cfml - A Guide to Writing API Wrappers
Presentation for Into the Box 2018
- 4,205