Rest api development
Collection of skills for the ColdBox Platform and Claude Plugin
npx -y skills add ColdBox/skills --skill rest-api-developmentAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use this skill when building RESTful APIs in ColdBox using RestHandler, creating CRUD API endpoints, implementing API versioning, handling JWT/bearer token authentication, building structured error responses, or creating resource representations with mementos.
SKILL.md
16.6 KB, ~4.0k tokens by cl100k_base, as published. Nobody here has run it
REST API Development
When to Use This Skill
Use this skill when building REST APIs with ColdBox, including API routing, handlers, authentication, versioning, and response formatting.
Language Mode Reference
Examples use BoxLang (.bx) syntax by default. Adapt for your target language:
| Concept | BoxLang (.bx) | CFML (.cfc) |
|---|---|---|
| Class declaration | class [extends="..."] { | component [extends="..."] { |
| DI annotation | @inject above property name="svc"; | property name="svc" inject="svc"; |
| View templates | .bxm suffix | .cfm / .cfml suffix |
| Tag prefix | <bx:if>, <bx:output>, <bx:set> | <cfif>, <cfoutput>, <cfset> |
CFML Compat Mode: With BoxLang + CFML Compat module,
.bxand.cfcfiles coexist freely. BoxLang-native classes useclass {}(.bxfiles); CFML-compat classes usecomponent {}(.cfcfiles).
Core Concepts
ColdBox REST APIs use:
- RestHandler — base handler with built-in error handling and response formatting
event.renderData()— renders JSON/XML/text responses with status codes- Route resources — maps CRUD verbs to handler actions
- Modules — ideal for versioned API isolation
- JWT/cbSecurity — provides API authentication
API Module Structure
modules_app/
api/
config/
Router.cfc # API-specific routes
handlers/
Users.cfc # Users API handler
Posts.cfc
models/
UserTransformer.cfc
ModuleConfig.cfc
RestHandler Base Class
class Users extends coldbox.system.RestHandler {
@inject
property name="userService";
// Runs before every action - great for input validation
function preHandler( event, rc, prc, action ) {
// Validate content type for mutation methods
if( !listFindNoCase( "index,show", action ) ){
if( !event.isPost() && !event.isPut() && !event.isPatch() ){
return
}
validateContentType( event )
}
}
/**
* GET /api/v1/users
*/
function index( event, rc, prc ) {
var users = userService.list(
page = rc.page ?: 1,
limit = rc.limit ?: 25,
filters = getFilters( rc )
)
event.renderData(
data = {
"data" : users.getRecords(),
"pagination" : users.getPagination()
},
statusCode = 200
)
}
/**
* GET /api/v1/users/:id
*/
function show( event, rc, prc ) {
var user = userService.getById( rc.id ?: 0 )
if( isNull( user ) ){
return apiNotFound( "User not found" )
}
event.renderData( data = user.getMemento(), statusCode = 200 )
}
/**
* POST /api/v1/users
*/
function create( event, rc, prc ) {
var payload = event.getHTTPContent( json = true )
var result = userService.create( payload )
if( result.hasErrors() ){
return apiValidationFailed( result.getErrors() )
}
event.renderData( data = result.getMemento(), statusCode = 201 )
}
/**
* PUT /api/v1/users/:id
*/
function update( event, rc, prc ) {
var payload = event.getHTTPContent( json = true )
var result = userService.update( rc.id ?: 0, payload )
if( isNull( result ) ){
return apiNotFound( "User not found" )
}
if( result.hasErrors() ){
return apiValidationFailed( result.getErrors() )
}
event.renderData( data = result.getMemento(), statusCode = 200 )
}
/**
* DELETE /api/v1/users/:id
*/
function delete( event, rc, prc ) {
var deleted = userService.delete( rc.id ?: 0 )
if( !deleted ){
return apiNotFound( "User not found" )
}
event.renderData( data = "", statusCode = 204 )
}
// Private helpers
private function getFilters( rc ) {
return {
search : rc.search ?: "",
role : rc.role ?: "",
active : rc.active ?: ""
}
}
private function apiNotFound( message ) {
event.renderData(
data = { "error": message, "statusCode": 404 },
statusCode = 404
)
}
private function apiValidationFailed( errors ) {
event.renderData(
data = {
"error" : "Validation failed",
"errors" : errors,
"statusCode" : 422
},
statusCode = 422
)
}
private function validateContentType( event ) {
if( event.getHTTPHeader( "Content-Type", "" ) does not contain "application/json" ){
event.renderData(
data = { "error": "Content-Type must be application/json" },
statusCode = 415
)
event.noRender( false )
}
}
}
CFML (.cfc):
component extends="coldbox.system.RestHandler" {
property name="userService" inject="userService";
// Runs before every action - great for input validation
function preHandler( event, rc, prc, action ) {
// Validate content type for mutation methods
if( !listFindNoCase( "index,show", action ) ){
if( !event.isPost() && !event.isPut() && !event.isPatch() ){
return
}
validateContentType( event )
}
}
/**
* GET /api/v1/users
*/
function index( event, rc, prc ) {
var users = userService.list(
page = rc.page ?: 1,
limit = rc.limit ?: 25,
filters = getFilters( rc )
)
event.renderData(
data = {
"data" : users.getRecords(),
"pagination" : users.getPagination()
},
statusCode = 200
)
}
/**
* GET /api/v1/users/:id
*/
function show( event, rc, prc ) {
var user = userService.getById( rc.id ?: 0 )
if( isNull( user ) ){
return apiNotFound( "User not found" )
}
event.renderData( data = user.getMemento(), statusCode = 200 )
}
/**
* POST /api/v1/users
*/
function create( event, rc, prc ) {
var payload = event.getHTTPContent( json = true )
var result = userService.create( payload )
if( result.hasErrors() ){
return apiValidationFailed( result.getErrors() )
}
event.renderData( data = result.getMemento(), statusCode = 201 )
}
/**
* PUT /api/v1/users/:id
*/
function update( event, rc, prc ) {
var payload = event.getHTTPContent( json = true )
var result = userService.update( rc.id ?: 0, payload )
if( isNull( result ) ){
return apiNotFound( "User not found" )
}
if( result.hasErrors() ){
return apiValidationFailed( result.getErrors() )
}
event.renderData( data = result.getMemento(), statusCode = 200 )
}
/**
* DELETE /api/v1/users/:id
*/
function delete( event, rc, prc ) {
var deleted = userService.delete( rc.id ?: 0 )
if( !deleted ){
return apiNotFound( "User not found" )
}
event.renderData( data = "", statusCode = 204 )
}
// Private helpers
private function getFilters( rc ) {
return {
search : rc.search ?: "",
role : rc.role ?: "",
active : rc.active ?: ""
}
}
private function apiNotFound( message ) {
event.renderData(
data = { "error": message, "statusCode": 404 },
statusCode = 404
)
}
private function apiValidationFailed( errors ) {
event.renderData(
data = {
"error" : "Validation failed",
"errors" : errors,
"statusCode" : 422
},
statusCode = 422
)
}
private function validateContentType( event ) {
if( event.getHTTPHeader( "Content-Type", "" ) does not contain "application/json" ){
event.renderData(
data = { "error": "Content-Type must be application/json" },
statusCode = 415
)
event.noRender( false )
}
}
}
API Module Configuration
// modules_app/api/ModuleConfig.cfc
class ModuleConfig {
property name="title" default="API Module";
property name="description" default="REST API";
property name="version" default="1.0.0";
property name="entryPoint" default="api";
property name="author" default="Ortus Solutions";
function configure() {
// Module settings
settings = {
jwtSecret : getSystemSetting( "JWT_SECRET", "" ),
tokenExpiry : 60, // minutes
refreshExpiry : 10080 // 7 days
}
}
}
// modules_app/api/config/Router.cfc
class Router extends coldbox.system.web.routing.Router {
function configure() {
// V1 resource routes
group( pattern = "/v1" ) {
resources( "users" )
resources( "posts" )
}
}
}
CFML (.cfc):
// modules_app/api/ModuleConfig.cfc
component {
property name="title" default="API Module";
property name="description" default="REST API";
property name="version" default="1.0.0";
property name="entryPoint" default="api";
property name="author" default="Ortus Solutions";
function configure() {
// Module settings
settings = {
jwtSecret : getSystemSetting( "JWT_SECRET", "" ),
tokenExpiry : 60, // minutes
refreshExpiry : 10080 // 7 days
}
}
}
// modules_app/api/config/Router.cfc
component extends="coldbox.system.web.routing.Router" {
function configure() {
// V1 resource routes
group( pattern = "/v1" ) {
resources( "users" )
resources( "posts" )
}
}
}
JWT Authentication
class Auth extends coldbox.system.RestHandler {
@inject
property name="authService";
@inject
property name="jwtService";
/**
* POST /api/v1/auth/login
*/
function login( event, rc, prc ) {
var credentials = event.getHTTPContent( json = true )
if( !authService.authenticate( credentials.email, credentials.password ) ){
event.renderData(
data = { "error": "Invalid credentials" },
statusCode = 401
)
return
}
var user = authService.getAuthenticatedUser()
var token = jwtService.fromUser( user )
event.renderData(
data = {
"access_token" : token,
"token_type" : "Bearer",
"expires_in" : 3600,
"user" : user.getMemento()
},
statusCode = 200
)
}
/**
* POST /api/v1/auth/refresh
*/
function refresh( event, rc, prc ) {
var bearer = getBearerToken( event )
var token = jwtService.refreshToken( bearer )
event.renderData(
data = { "access_token": token, "token_type": "Bearer" },
statusCode = 200
)
}
/**
* POST /api/v1/auth/logout
*/
function logout( event, rc, prc ) {
var bearer = getBearerToken( event )
jwtService.invalidateToken( bearer )
event.renderData( data = { "message": "Logged out" }, statusCode = 200 )
}
private function getBearerToken( event ) {
var header = event.getHTTPHeader( "Authorization", "" )
return replaceNoCase( header, "Bearer ", "", "one" )
}
}
CFML (.cfc):
component extends="coldbox.system.RestHandler" {
property name="authService" inject="authService";
property name="jwtService" inject="jwtService";
/**
* POST /api/v1/auth/login
*/
function login( event, rc, prc ) {
var credentials = event.getHTTPContent( json = true )
if( !authService.authenticate( credentials.email, credentials.password ) ){
event.renderData(
data = { "error": "Invalid credentials" },
statusCode = 401
)
return
}
var user = authService.getAuthenticatedUser()
var token = jwtService.fromUser( user )
event.renderData(
data = {
"access_token" : token,
"token_type" : "Bearer",
"expires_in" : 3600,
"user" : user.getMemento()
},
statusCode = 200
)
}
/**
* POST /api/v1/auth/refresh
*/
function refresh( event, rc, prc ) {
var bearer = getBearerToken( event )
var token = jwtService.refreshToken( bearer )
event.renderData(
data = { "access_token": token, "token_type": "Bearer" },
statusCode = 200
)
}
/**
* POST /api/v1/auth/logout
*/
function logout( event, rc, prc ) {
var bearer = getBearerToken( event )
jwtService.invalidateToken( bearer )
event.renderData( data = { "message": "Logged out" }, statusCode = 200 )
}
private function getBearerToken( event ) {
var header = event.getHTTPHeader( "Authorization", "" )
return replaceNoCase( header, "Bearer ", "", "one" )
}
}
API Versioning
// Using modules for versioning
// modules_app/apiV1/ModuleConfig.cfc
class ModuleConfig {
property name="entryPoint" default="api/v1";
}
// modules_app/apiV2/ModuleConfig.cfc
class ModuleConfig {
property name="entryPoint" default="api/v2";
}
// URL-based versioning
group( pattern = "/api/v1" ) {
resources( "users" )
}
group( pattern = "/api/v2" ) {
resources( "users" )
}
// Header-based versioning
function preHandler( event, rc, prc, action ) {
var version = event.getHTTPHeader( "Accept-Version", "v1" )
prc.apiVersion = version
}
CFML (.cfc):
// Using modules for versioning
// modules_app/apiV1/ModuleConfig.cfc
component {
property name="entryPoint" default="api/v1";
}
// modules_app/apiV2/ModuleConfig.cfc
component {
property name="entryPoint" default="api/v2";
}
// URL-based versioning
group( pattern = "/api/v1" ) {
resources( "users" )
}
group( pattern = "/api/v2" ) {
resources( "users" )
}
// Header-based versioning
function preHandler( event, rc, prc, action ) {
var version = event.getHTTPHeader( "Accept-Version", "v1" )
prc.apiVersion = version
}
Pagination Response Structure
function index( event, rc, prc ) {
var page = rc.page ?: 1
var limit = min( rc.limit ?: 25, 100 ) // cap at 100
var results = userService.paginate(
offset = ( page - 1 ) * limit,
limit = limit
)
event.renderData(
data = {
"data" : results.records,
"meta" : {
"total" : results.total,
"page" : page,
"limit" : limit,
"total_pages" : ceiling( results.total / limit ),
"has_more" : ( page * limit ) < results.total
},
"links" : {
"self" : buildLink( event.getCurrentEvent(), { page: page, limit: limit } ),
"next" : ( page * limit ) < results.total ? buildLink( event.getCurrentEvent(), { page: page + 1, limit: limit } ) : "",
"prev" : page > 1 ? buildLink( event.getCurrentEvent(), { page: page - 1, limit: limit } ) : ""
}
},
statusCode = 200
)
}
REST API Best Practices
- Use
RestHandleroverEventHandlerfor APIs — it adds proper error handling - Use HTTP status codes correctly (200, 201, 204, 400, 401, 403, 404, 422, 500)
- Read request body with
event.getHTTPContent( json = true )for POST/PUT - Structure responses consistently — always return
data+ optionalmeta+ optionallinks - Use modules for API versioning to keep versions independent
- Validate inputs before processing — return 422 with errors structure
- Secure endpoints with JWT + cbSecurity
@securedannotations - Use CORS interceptors for cross-origin access
Gives 0 of the 12 instructions most apis services skills give in ~4.0k tokens
Counted across 424 of the 426 authors here whose files we hold, read 2026-08-06
- use plural nouns for resource namesin 41 of 424, across 32 files
- use cursor-based pagination for large datasetsin 35 of 424, across 20 files
- include rate limit headers in responsesin 25 of 424, across 13 files
- Use kebab-case for multi-word resourcesin 23 of 424, across 13 files
- version APIs in the URL pathin 19 of 424, across 9 files
- use semantic HTTP status codesin 18 of 424, across 8 files
- verify webhook signaturesin 18 of 424, across 11 files
- use query parameters for filteringin 17 of 424, across 6 files
- use async database operationsin 14 of 424, across 7 files
- wrap successful responses in a data fieldin 13 of 424, across 3 files
- prefix sorting parameters with a hyphen for descending orderin 13 of 424, across 3 files
- set appropriate HTTP status codesin 13 of 424, across 6 files
Said here and by no other author read
- extend the RestHandler base class
- render responses using event.renderData()
- map CRUD verbs to resource routes
- isolate API versions inside modules
- validate content type for mutation methods
- use preHandler for input validation
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.