A REST API in 5 minutes with Preside

About me

Published my first website in 1994
http://mail.bris.ac.uk/~cl3018
(Yes, it was hosted on a mail server...)

First used Allaire ColdFusion in 1999

Lead developer at Pixl8
One of the lead developers on Preside

Stand-alone websites

Data feeds over FTP

WSDL, SOAP XML etc.

REST APIs

SaaS applications

Web apps

Embedded widgets

Mobile apps

Quick Start

Start the clock...

1.
Install the extension


$ box install preside-ext-data-api
					

2.
Configure a data object

/preside-objects/dog.cfc

/**
 *
 */

component {
  property name="name" type="string" dbtype="varchar";
  // more properties...
}
/preside-objects/dog.cfc

/**
 * @dataApiEnabled  true
 */

component {
  property name="name" type="string" dbtype="varchar";
  // more properties...
}

3.
Create an API user

...and we’re done!

Auto-generated docs

/api/data/v1/docs/html/

GET /entity/dog/

GET /entity/dog/{recordId}/

PUT /entity/dog/{recordId}/

POST /entity/dog/

Preside’s approach to REST

The Preside Data API uses four verbs:

  • GET
  • POST
  • PUT
  • DELETE

PATCH is not used

All updates use PUT

Only supplied fields are updated

Response body contains only the data

Metadata is passed in the headers

API is documented separately

REST responses

200: Success

401: Authorization failed

404: Not found

405: Method not supported

422: Validation failure

500: Server error

Customising objects

/base/api_entity.cfc

/**
 * @dataApiEnabled             true
 * @dataApiExcludeFields       _version_is_draft, 
       _version_has_drafts
 * @dataApiUpsertExcludeFields _version_is_draft, 
       _version_has_drafts,datemodified,datecreated
 */

component {}
/base/api_entity.cfc

/**
 * @dataApiEnabled             true
 * @dataApiExcludeFields       _version_is_draft, 
       _version_has_drafts
 * @dataApiUpsertExcludeFields _version_is_draft, 
       _version_has_drafts,datemodified,datecreated
 */

component {}
/base/api_entity.cfc

/**
 * @dataApiEnabled             true
 * @dataApiExcludeFields       _version_is_draft, 
       _version_has_drafts
 * @dataApiUpsertExcludeFields _version_is_draft, 
       _version_has_drafts,datemodified,datecreated
 */

component {}
/base/api_entity.cfc

/**
 * @dataApiEnabled             true
 * @dataApiExcludeFields       _version_is_draft, 
       _version_has_drafts
 * @dataApiUpsertExcludeFields _version_is_draft, 
       _version_has_drafts,datemodified,datecreated
 */

component {}
/preside-objects/dog.cfc

/**
 * @dataApiEnabled      true
 */


component {
  property name="name" type="string" dbtype="varchar";
  // more properties...
}
/preside-objects/dog.cfc

/**
 */



component {
  property name="name" type="string" dbtype="varchar";
  // more properties...
}
/preside-objects/dog.cfc

/**
 */



component extends="app.base.api_entity" {
  property name="name" type="string" dbtype="varchar";
  // more properties...
}
/preside-objects/dog.cfc

/**
 * @dataApiEntityName   dogs
 */


component extends="app.base.api_entity" {
  property name="name" type="string" dbtype="varchar";
  // more properties...
}
/preside-objects/dog.cfc

/**
 * @dataApiEntityName   dogs
 * @dataApiCategory     core
 */

component extends="app.base.api_entity" {
  property name="name" type="string" dbtype="varchar";
  // more properties...
}
/preside-objects/breed.cfc

/**
 * @dataManagerGroup   dogs
 * @dataApiCategory    lookups
 * @dataApiEntityName  breeds
 */


component extends="app.base.api_entity" {}
/preside-objects/breed.cfc

/**
 * @dataManagerGroup   dogs
 * @dataApiCategory    lookups
 * @dataApiEntityName  breeds
 * @dataApiVerbs       GET,POST
 */

component extends="app.base.api_entity" {}

Object annotations

  • dataApiEntityName
  • dataApiCategory
  • dataApiQueueEnabled
  • dataApiQueue
  • dataApiSortOrder
  • dataApiSavedFilters
  • dataApiVerbs
  • dataApiFields
  • dataApiUpsertFields
  • dataApiExcludeFields
  • dataApiUpsertExcludeFields
  • dataApiFilterFields
  • dataApiAllowIdInsert

Customising properties

/preside-objects/dog.cfc

component {
  property name="name" type="string" dbtype="varchar";
  property name="main_photo"
      relationship="many-to-one" relatedTo="asset";
  property name="status"
      relationship="many-to-one" relatedTo="homing_status";
  property name="location"
      relationship="many-to-one" relatedTo="location";
  // more properties...
}
 
 
 
 
/preside-objects/dog.cfc

component {
  property name="name" type="string" dbtype="varchar";
  property name="main_photo"
      relationship="many-to-one" relatedTo="asset"
      dataApiAlias="photoUrl";
  property name="status"
      relationship="many-to-one" relatedTo="homing_status";
  property name="location"
      relationship="many-to-one" relatedTo="location";
  // more properties...
}
 
 
 
/preside-objects/dog.cfc

component {
  property name="name" type="string" dbtype="varchar";
  property name="main_photo"
      relationship="many-to-one" relatedTo="asset"
      dataApiAlias="photoUrl"
      dataApiDerivative="dogListingPhoto";
  property name="status"
      relationship="many-to-one" relatedTo="homing_status";
  property name="location"
      relationship="many-to-one" relatedTo="location";
  // more properties...
}
 
 
/preside-objects/dog.cfc

component {
  property name="name" type="string" dbtype="varchar";
  property name="main_photo"
      relationship="many-to-one" relatedTo="asset"
      dataApiAlias="photoUrl"
      dataApiDerivative="dogListingPhoto";
  property name="status"
      relationship="many-to-one" relatedTo="homing_status"
      dataApiRenderer="dataApiHomingStatus";
  property name="location"
      relationship="many-to-one" relatedTo="location"
      dataApiRenderer="dataApiLocation";
  // more properties...
}
/handlers/renderers/content/dataApiHomingStatus.cfc

component {

  private string function default( event, rc, prc, args={} ){
    var statusId = args.data ?: "";

    return renderLabel( "homing_status", statusId );
  }
}
 
 
 
/handlers/renderers/content/dataApiLocation.cfc

component {

  private struct function default( event, rc, prc, args={} ){
    var locationId = args.data ?: "";

    return {
        id    = locationId
      , label = renderLabel( "location", locationId )
    };
  }
}

Property annotations

  • dataApiAlias
  • dataApiRenderer
  • dataApiDerivative
  • dataApiType
  • dataApiFormat
  • dataApiEnabled
  • dataApiUpsertEnabled

Customising the Docs

/i18n/dataapi.properties

api.title=Rescue Remedies API
api.description=This API provides REST access to the data for the Rescue Remedies dog rehoming website.
api.version=v1.0
api.favicon=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAPoAAAD6CAYAAACI7Fo9AAAGVGlUWH...
					
/i18n/dataapi.properties

# OBJECT LEVEL
entity.dogs.name=Dogs
entity.dogs.name.singular=Dog
entity.dogs.description=Here are all the dogs...
entity.dogs.sort.order=10

operation.dogs.get.description=Description for the paginated GET operation for your entity

# FIELD LEVEL
entity.dogs.field.location.description=ID and label of the location
					

Data change queues

/config/Config.cfc

settings.rest.apis[ "/data/v1" ].dataApiQueueEnabled = true;
settings.rest.apis[ "/data/v1" ].dataApiQueues = {
    default     = { pageSize=100, atomicChanges=true  }
  , lowpriority = { pageSize=10 , atomicChanges=false }
};
					

Authentication

Basic authentication

Users manually created in admin

Access token passed in as username

Potential for self-service by website users

Please use HTTPS!

No authentication

WARNING!
Only use for a read-only API

/config/Config.cfc

settings.rest.apis[ "/data/v1" ].authProvider = "";
						

Multiple APIs

Surely one API is enough?

Public/private API

API versioning

Extension-specific API

Other separation of logic

/config/Config.cfc

settings.rest.apis[ "/public/v1" ] = {
    authProvider        = ""
  , description         = "Public REST API"
  , dataApiNamespace    = "public"
  , dataApiQueueEnabled = false
};
settings.rest.apis[ "/public/v1/docs" ] = {
    description      = "Documentation for Public REST API"
  , dataApiNamespace = "public"
  , dataApiDocs      = true
};
					
/preside-objects/dog.cfc

/**
 * @labelField                  name
 * @dataApiEnabled              true
 */





component {

}
					
/preside-objects/dog.cfc

/**
 * @labelField                  name
 * @dataApiEnabled              true
 * @dataApiEnabled:public       true
 */




component {

}
					
/preside-objects/dog.cfc

/**
 * @labelField                  name
 * @dataApiEnabled              true
 * @dataApiEnabled:public       true
 * @dataApiVerbs:public         GET
 */



component {

}
					
/preside-objects/dog.cfc

/**
 * @labelField                  name
 * @dataApiEnabled              true
 * @dataApiEnabled:public       true
 * @dataApiVerbs:public         GET
 * @dataApiFields:public        id,name,main_photo,status,location,breed,cross_breed,cross_with,gender,neutered,age,story
 */


component {

}
					
/preside-objects/dog.cfc

/**
 * @labelField                  name
 * @dataApiEnabled              true
 * @dataApiEnabled:public       true
 * @dataApiVerbs:public         GET
 * @dataApiFields:public        id,name,main_photo,status,location,breed,cross_breed,cross_with,gender,neutered,age,story
 * @dataApiFilterFields:public  name,status,breed,gender
 */

component {

}
					
/preside-objects/dog.cfc

component {
  property name="main_photo";
  property name="status";
  property name="location";
  // more properties...
}
 
 
 
 
 
 
 
 
					
/preside-objects/dog.cfc

component {
  property name="main_photo"
      dataApiAlias:public="photo"
      dataApiDerivative:public="dogListingPhoto";
  property name="status";
  property name="location";
  // more properties...
}
 
 
 
 
 
 
					
/preside-objects/dog.cfc

component {
  property name="main_photo"
      dataApiAlias:public="photo"
      dataApiDerivative:public="dogListingPhoto";
  property name="status"
      dataApiRenderer:public="dataApiHomingStatus"
      dataApiType:public="string"
      dataApiFormat:public="";
  property name="location";
  // more properties...
}
 
 
 
					
/preside-objects/dog.cfc

component {
  property name="main_photo"
      dataApiAlias:public="photo"
      dataApiDerivative:public="dogListingPhoto";
  property name="status"
      dataApiRenderer:public="dataApiHomingStatus"
      dataApiType:public="string"
      dataApiFormat:public="";
  property name="location"
      dataApiRenderer:public="dataApiLocation"
      dataApiType:public="string"
      dataApiFormat:public="";
  // more properties...
}
					
/api/data/v1/docs/html/
/api/public/v1/docs/html/
/api/public/v1/docs/html/
/api/public/v1/docs/html/

Further customisations

Custom authenticator

Custom endpoints

Interceptors

Interception points

  • onOpenApiSpecGeneration
  • [pre|post]DataApiSelectData
  • [pre|post]DataApiInsertData
  • [pre|post]DataApiUpdateData
  • [pre|post]DataApiDeleteData

Recap

Uses your existing data model

Extensive configuration options

Auto-generated docs

Multiple APIs

Data change queues

Powerful customisations

Further reading

  • Preside Data API docs
    https://github.com/pixl8/preside-ext-data-api
  • REST Assured: A Pragmatic Approach To API Design
    Adam Tuttle
    https://restassuredbook.com/
  • Build APIs You Won’t Hate
    Phil Sturgeon
    https://apisyouwonthate.com/

Get in touch

  • Twitter
    @sebduggan
  • Slack
    https://www.preside.org/slack
  • Slides from this talk
    https://sebduggan.github.io/api-in-5-minutes/
@sebduggan