REST framework
Introduction
Preside provides a framework for developing REST APIs that work inline and seamlessly with the rest of the ecosystem. It has taken inspiration from the Taffy REST Framework by Adam Tuttle, and follows several of its patterns.
The current version of the framework provides you with the conventions, services and routing layer to help you easily author your own REST APIs; further tooling such as documentation generation and user management are planned for future releases.
Info
The documentation here will not attempt to teach the ins and outs of RESTful APIs; rather document how Preside implements RESTful concepts. We can highly recommend Adam Tuttle's book, REST Assured as a primer and go-to resource for authoring REST APIs.
APIs and Resources
Creating a new REST API in Preside is a case of creating a directory containing coldbox handler CFCs. Each handler represents a resource in your API. These APIs and resources must all live under your application's /handlers/rest-apis/
folder. For example:
/application/handlers/rest-apis
/my-cool-api
/v1
SomeResource.cfc
The structure above defines a resource, SomeResource
, beneath the /my-cool-api/v1
API.
Defining a resource
Resource CFCs are simple ColdBox handlers with some additional annotations to define how they should work within the REST API. An example:
/**
* @restUri /someresource/{variable}/{variable2}/
*
*/
component {
property name="pageDao" inject="presidecms:object:page";
private void function get( required string variable, required string variable2 ) {
var records = someDao.selectData(
selectFields = [ "id", "title" ]
, savedFilters = [ "livePages" ]
);
restResponse.setData( QueryToArray( records ) )
.setStatus( 200, "Awesome" )
.setHeader( "X-Rocking", true );
}
private void function post( required string variable, required string variable2 ) {
// ...
}
/**
* @restVerb push
*
*/
private void function anotherNameForPush( required string variable, required string variable2 ) {
// ...
}
// etc.
}
Routing and the REST URI definition
The @restUri
annotation defines URL patterns that will be matched by this resource. It can optionally contain wildcards that map to variable names indicated by curly braces {somevariable}
. Individual patterns are separated with a comma.
The entire URL path for routing a REST request to a resource will be made up of three parts:
- The configured REST path that tells Preside that this is a REST request. The default is
/api
. - The path to the specific API that the resource lives under, i.e. the folder structure beneath
/handlers/rest-apis
- The path that will match the specific resource
For example, if your resource lived at /handlers/rest-apis/myapi/v1/Page.cfc
and defined the @restUri
pattern as /pages/,/pages/{slug}/{pageid}/
, it would match the following URL paths:
/api/myapi/v1/pages/
/api/myapi/v1/pages/some-slug/359860837568/
Tip
You can configure the path that the framework uses to recognize rest requests by setting the settings.rest.path
variable in your site's Config.cfc
file. e.g. settings.rest.path = "/rest";
.
Mapping HTTP Methods (Verbs) to resource handler actions
By providing methods on your resource CFC that match the names of HTTP Methods, you can route a request to a specific function based on the HTTP method used by the request. For example, to handle a request to your resources URI using the HTTP DELETE method, you would implement a delete
handler action:
/**
* @restUri /blogcategories/,/blogcategories/{slug}/{id}/
*
*/
component {
property name="blogCategoryDao" inject="presidecms:object:blog_category";
private void function delete( required string id ) {
blogCategoryDao.deleteData( id=arguments.id );
restResponse.noData().setStatus( 200, "OK" );
}
}
Using different method names
If you prefer, or need, to use different method names, you can map HTTP methods to your handler actions with the @restVerb
annotation against the handler action itself. e.g. here we map the deleteCategory
method to the DELETE
verb:
/**
* @restUri /blogcategories/,/blogcategories/{slug}/{id}/
*
*/
component {
property name="blogCategoryDao" inject="presidecms:object:blog_category";
/**
* @restVerb DELETE
*
*/
private void function deleteCategory( required string id ) {
blogCategoryDao.deleteData( id=arguments.id );
restResponse.noData().setStatus( 200, "OK" );
}
}
Accepting arguments
Because your REST API resources are defined as ColdBox handlers, your handler actions will always receive the usual event
, rc
and prc
arguments.
REST Request and Response objects
In addition to the standard ColdBox arguments, the REST framework provides your handler action with restRequest
and restResponse
arguments. You can use the restResponse
object to set data, mime type, renderer, status code and HTTP headers for the response of the REST request. The restRequest
argument can be used to discover information about the request, and to prematurely end the request with restRequest.finish()
.
See the reference docs for Preside REST Request and Preside REST Response for full details.
/**
* @restUri /events/
*
*/
component {
private void function get() {
restResponse.setError(
errorCode = 501
, title = "Not implemented"
, message = "The /events/ GET api has not yet been implemented."
);
}
}
Tip
We prefer not to include the event
, rc
, prc
, restRequest
and restResponse
arguments in the function definition to help with readability.
REST URI Tokens
If your resource defines a URI mapping that includes tokens, these will also be passed to your handler actions when available, for instance:
/**
* @restUri /events/,/events/{id}/
*
*/
component {
// here, the 'id' argument is automatically
// passed to the action when it is present
// in the rest URI
private void function get( string id="" ) {
// ...
}
}
URL Parameters
Finally, any query string or POST parameters will also be available as individual arguments (in addition to being available in rc
). This will help future development in the API where we would like to automatically raise friendly errors for missing parameters, etc.
For example:
/**
* @restUri /events/,/events/{id}/
*
*/
component {
private void function get(
string id = ""
, numeric page = 1
, numeric pageSize = 50
) {
// here we expect URLs like /events/?page=3&pageSize=10
// or /events/34583745/
}
}
Configuring your APIs
Any additional configuration of the REST APIs can be made in your site's Config.cfc
file. There is a core settings structure for REST that looks like:
settings.rest = {
path = "/api"
, corsEnabled = false
, apis = {}
};
Additional settings can be defined either globally, or per API. Currently there is only a single setting, corsEnabled
which is turned off by default. An example of turning CORS on globally would look like this:
settings.rest.corsEnabled = true
Or, to turn it on only for a specific API:
settings.rest.apis[ "/myapi/v2" ] = { corsEnabled=true };
Basic caching
The framework automatically adds ETag
response headers for GET and HEAD REST requests. These are a simple MD5 hash of the serialized response body. In addition, if the REST request includes a If-None-Match
request header whose value matches the generated ETag
, the framework will set an empty response body and set the status of the response to 304 Not modified
.
More advanced caching can be achieved using the CacheBox framework that is built in to ColdBox (and therefore Preside). See the ColdBox docs for further details.
HEAD requests
The framework deals with HEAD requests for you, without you needing to implement a resource handler action for the verb. Simply, when responding to a HEAD request, the system will call the GET action for your resource and empty the body data before rendering the response.
CORS support
CORS (Cross-Origin Request Sharing) is used to validate that a resource can be used by a system from another origin. This is relevant for browser based JavaScript requests to your API where the requesting page resides at a domain that differs to that of the API.
Before requesting the remote resource fully, a browser will send a "pre-flight" request using the OPTIONS
HTTP Method along with headers to describe the intentions of the upcoming request. The Preside Rest framework detects these requests for you and responds appropriately based on:
- Whether or not CORS is enabled for the API (currently, we only allow enabling or disabling CORS globally for all domains)
- Whether or not the matching resource supplies a method for responding to the given HTTP Method
If the framework detects an OPTIONS
request without the prerequisite CORS headers, it will respond with a 400 Bad request
status. If the request is valid, but CORS disallowed for either of the reasons above, a 403 Forbidden
status will be returned. Finally, if the request is valid and the CORS request allowed, a 200 OK
status will be returned, along with the relevant Access-Control
response headers to inform the calling system that the CORS request is valid.
Interception points
Your application can listen into several core interception points to enhance the features of the REST platform, e.g. to implement custom authentication. See the ColdBox Interceptor's documentation for detailed documentation on interceptors.
For example, an interceptor that listens for the onUnsupportedRestMethod
interception point and changes the REST response to something other than the default:
component extends="coldbox.system.Interceptor" {
public void function configure() {}
public void function onUnsupportedRestMethod( event, interceptData ) {
var response = interceptData.restResponse;
response.setStatus( 405, "This is not the method you are looking for" )
.setBody( "nope" )
.setRenderer( "plain" )
.setMimeType( "text/plain" );
}
}
The Interception points are:
onRestRequest
Fired at the beginning of a REST request. Takes restRequest
and restResponse
objects as arguments.
onRestError
Fired whenever an unhandled exception occurs during execution of the request. Takes error
, restRequest
and restResponse
objects as arguments.
onMissingRestResource
Fired when no resource matches the incoming URL Path. Takes restRequest
and restResponse
objects as arguments.
onUnsupportedRestMethod
Fired when the matched resource does not support the used HTTP Method. Takes restRequest
and restResponse
objects as arguments.
preInvokeRestResource
Fired before the resource's handler action is called. Takes args
structure, and restRequest
and restResponse
objects as arguments. The args
structure are the arguments that will be passed to the resource's handler action.
postInvokeRestResource
Fired after the resource's handler action is called. Takes args
structure, and restRequest
and restResponse
objects as arguments. The args
structure represents the arguments that were passed to the resource's handler action.
Authentication
The REST framework comes with a system for providing authentication handlers that can optionally be configured through a user interface.
Creating an authentication provider
An authentication provider is made up of:
- A convention based handler providing the authentication logic and optional configuration logic
- A convention based i18n file to provide user friendly text for the provider
Note, in order for configuration to be activated, the apiManager
feature is required (settings.features.apiManager.enabled = true
).
The handler
Create a handler at /handlers/rest/auth/{IdOfProvider}.cfc
. Example (from core "Token" provider):
/**
* Handler for authenticating with token authentication
*
*/
component {
property name="authService" inject="presideRestAuthService";
/**
* Invoked at the start of any REST API request
* for a REST api configured to use this authentication
* provider
*
*/
private string function authenticate() {
var headers = getHTTPRequestData( false ).headers;
var authHeader = headers.Authorization ?: "";
var token = "";
try {
authHeader = toString( toBinary( listRest( authHeader, ' ' ) ) );
token = ListFirst( authHeader, ":" );
if ( !token.trim().len() ) {
throw( type="missing.token" );
}
} catch( any e ) {
// returning empty string, not authenticated
return "";
}
var userId = authService.getUserIdByToken( token );
if ( userId.len() && authService.userHasAccessToApi( userId, restRequest.getApi() ) ) {
// if authentication is successful, return ID of the user
return userId;
}
// returning empty string, not authenticated
return "";
}
/**
* Invoked when a user clicks on "configure" link in the API manager
* besides the API they wish to configure
*
*/
private string function configure() {
setNextEvent( url=event.buildAdminLink( "apiusermanager" ) );
}
}
i18n file
Create a .properties
file at /i18n/rest/auth/{IdOfProvider}.properties
. e.g. (from core Token provider):
title=Basic token authentication
description=REST users are assigned tokens that can be used to authenticate
iconClass=fa-tag
Using an authentication provider
To make use of a custom authentication provider, you must configure your REST api in Config.cfc. For example, if you have a REST API at /handlers/rest-apis/my-api/v1
and wish to use the built-in "token" authentication provider:
settings.rest.apis[ "/my-api/v1" ] = {
authProvider = "token"
, description = "My API with its lovely description"
}
Getting the user ID during a REST request
In any REST route handler, you are able to get the ID of the authenticated user with restRequest.getUser()
. This will be the user ID as returned from the authenticate()
method of your authentication provider's handler.