CLI Overview
Quick Start Guide
wheels info
wheels reload
wheels deps
wheels destroy
wheels watch
wheels generate app
wheels generate app-wizard
wheels generate controller
wheels generate model
wheels generate view
wheels generate property
wheels generate route
wheels generate resource
wheels generate api-resource
wheels generate frontend
wheels generate test
wheels generate snippets
wheels scaffold
wheels db create
wheels db drop
wheels db setup
wheels db reset
wheels db status
wheels db version
wheels db rollback
wheels db seed
wheels db dump
wheels db restore
wheels db shell
wheels db schema
wheels dbmigrate info
wheels dbmigrate latest
wheels dbmigrate up
wheels dbmigrate down
wheels dbmigrate reset
wheels dbmigrate exec
wheels dbmigrate create blank
wheels dbmigrate create table
wheels dbmigrate create column
wheels dbmigrate remove table
wheels test
wheels test run
wheels test coverage
wheels test debug
wheels config list
wheels config set
wheels config env
wheels env
wheels env setup
wheels env list
wheels env switch
wheels environment
wheels console
wheels runner
wheels server
wheels server start
wheels server stop
wheels server restart
wheels server status
wheels server log
wheels server open
wheels plugins
wheels plugins list
wheels plugins install
wheels plugins remove
wheels analyze
wheels analyze code
wheels analyze performance
wheels analyze security
wheels security
wheels security scan
wheels optimize
wheels optimize performance
wheels docs
wheels docs generate
wheels docs serve
wheels ci init
wheels docker init
wheels docker deploy
wheels deploy
wheels deploy audit
wheels deploy exec
wheels deploy hooks
wheels deploy init
wheels deploy lock
wheels deploy logs
wheels deploy proxy
wheels deploy push
wheels deploy rollback
wheels deploy secrets
wheels deploy setup
wheels deploy status
wheels deploy stop
Configuration Management
Creating Commands
Service Architecture
Migrations Guide
Testing Guide
Object Relational Mapping
Creating Records
Reading Records
Updating Records
Deleting Records
Column Statistics
Dynamic Finders
Getting Paginated Data
Associations
Nested Properties
Object Validation
Object Callbacks
Calculated Properties
Transactions
Dirty Records
Soft Delete
Automatic Time Stamps
Using Multiple Data Sources
wheels generate api-resource (Coming Soon)
This command may not work as expected. A complete and stable version is coming soon.
Generate a complete API resource with model, API controller, and routes.
⚠️ Note: This command is currently marked as broken/disabled in the codebase. The documentation below represents the intended functionality when the command is restored.
Synopsis
wheels generate api-resource [name] [properties] [options]
wheels g api-resource [name] [properties] [options]
Description
The wheels generate api-resource
command creates a complete RESTful API resource including model, API-specific controller (no views), routes, and optionally database migrations and tests. It's optimized for building JSON APIs following REST conventions.
Current Status
This command is temporarily disabled. Use alternative approaches:
# Option 1: Use regular resource with --api flag
wheels generate resource product name:string price:float --api
# Option 2: Generate components separately
wheels generate model product name:string price:float
wheels generate controller api/products --api
wheels generate route products --api --namespace=api
Arguments (When Enabled)
| Argument | Description | Default |
|----------|-------------|---------|
| name
| Resource name (typically singular) | Required |
| properties
| Property definitions (name:type) | |
Options (When Enabled)
| Option | Description | Default |
|--------|-------------|---------|
| --version
| API version (v1, v2, etc.) | v1
|
| --format
| Response format (json, xml) | json
|
| --auth
| Include authentication | false
|
| --pagination
| Include pagination | true
|
| --filtering
| Include filtering | true
|
| --sorting
| Include sorting | true
|
| --skip-model
| Skip model generation | false
|
| --skip-migration
| Skip migration generation | false
|
| --skip-tests
| Skip test generation | false
|
| --namespace
| API namespace | api
|
| --force
| Overwrite existing files | false
|
| --help
| Show help information | |
Intended Functionality
Basic API Resource
wheels generate api-resource product name:string price:float description:text
Would generate:
- Model:
/models/Product.cfc
- Controller:
/controllers/api/v1/Products.cfc
- Route: API namespace with versioning
- Migration: Database migration file
- Tests: API integration tests
Generated API Controller
/controllers/api/v1/Products.cfc
:
component extends="Controller" {
function init() {
provides("json");
// Filters
filters(through="authenticate", except="index,show");
filters(through="findProduct", only="show,update,delete");
filters(through="parseApiParams", only="index");
}
function index() {
// Pagination
page = params.page ?: 1;
perPage = Min(params.perPage ?: 25, 100);
// Filtering
where = [];
if (StructKeyExists(params, "filter")) {
if (StructKeyExists(params.filter, "name")) {
ArrayAppend(where, "name LIKE :name");
params.name = "%#params.filter.name#%";
}
if (StructKeyExists(params.filter, "minPrice")) {
ArrayAppend(where, "price >= :minPrice");
params.minPrice = params.filter.minPrice;
}
}
// Sorting
order = "createdAt DESC";
if (StructKeyExists(params, "sort")) {
order = parseSort(params.sort);
}
// Query
products = model("Product").findAll(
where=ArrayToList(where, " AND "),
order=order,
page=page,
perPage=perPage,
returnAs="objects"
);
// Response
renderWith({
data: serializeProducts(products),
meta: {
pagination: {
page: products.currentPage,
perPage: products.perPage,
total: products.totalRecords,
pages: products.totalPages
}
},
links: {
self: urlFor(route="apiV1Products", params=params),
first: urlFor(route="apiV1Products", params=params, page=1),
last: urlFor(route="apiV1Products", params=params, page=products.totalPages),
prev: products.currentPage > 1 ? urlFor(route="apiV1Products", params=params, page=products.currentPage-1) : "",
next: products.currentPage < products.totalPages ? urlFor(route="apiV1Products", params=params, page=products.currentPage+1) : ""
}
});
}
function show() {
renderWith({
data: serializeProduct(product),
links: {
self: urlFor(route="apiV1Product", key=product.id)
}
});
}
function create() {
product = model("Product").new(deserializeProduct(params));
if (product.save()) {
renderWith(
data={
data: serializeProduct(product),
links: {
self: urlFor(route="apiV1Product", key=product.id)
}
},
status=201,
headers={"Location": urlFor(route="apiV1Product", key=product.id)}
);
} else {
renderWith(
data={
errors: formatErrors(product.allErrors())
},
status=422
);
}
}
function update() {
if (product.update(deserializeProduct(params))) {
renderWith({
data: serializeProduct(product),
links: {
self: urlFor(route="apiV1Product", key=product.id)
}
});
} else {
renderWith(
data={
errors: formatErrors(product.allErrors())
},
status=422
);
}
}
function delete() {
if (product.delete()) {
renderWith(data={}, status=204);
} else {
renderWith(
data={
errors: [{
status: "400",
title: "Bad Request",
detail: "Could not delete product"
}]
},
status=400
);
}
}
// Private methods
private function findProduct() {
product = model("Product").findByKey(params.key);
if (!IsObject(product)) {
renderWith(
data={
errors: [{
status: "404",
title: "Not Found",
detail: "Product not found"
}]
},
status=404
);
}
}
private function authenticate() {
if (!StructKeyExists(headers, "Authorization")) {
renderWith(
data={
errors: [{
status: "401",
title: "Unauthorized",
detail: "Missing authentication"
}]
},
status=401
);
}
// Implement authentication logic
}
private function parseApiParams() {
// Parse JSON API params
if (StructKeyExists(params, "_json")) {
StructAppend(params, params._json, true);
}
}
private function parseSort(required string sort) {
local.allowedFields = ["name", "price", "createdAt"];
local.parts = ListToArray(arguments.sort);
local.order = [];
for (local.part in local.parts) {
local.desc = Left(local.part, 1) == "-";
local.field = local.desc ? Right(local.part, Len(local.part)-1) : local.part;
if (ArrayFindNoCase(local.allowedFields, local.field)) {
ArrayAppend(local.order, local.field & (local.desc ? " DESC" : " ASC"));
}
}
return ArrayToList(local.order);
}
private function serializeProducts(required array products) {
local.result = [];
for (local.product in arguments.products) {
ArrayAppend(local.result, serializeProduct(local.product));
}
return local.result;
}
private function serializeProduct(required any product) {
return {
type: "products",
id: arguments.product.id,
attributes: {
name: arguments.product.name,
price: arguments.product.price,
description: arguments.product.description,
createdAt: DateTimeFormat(arguments.product.createdAt, "iso"),
updatedAt: DateTimeFormat(arguments.product.updatedAt, "iso")
},
links: {
self: urlFor(route="apiV1Product", key=arguments.product.id)
}
};
}
private function deserializeProduct(required struct data) {
if (StructKeyExists(arguments.data, "data")) {
return arguments.data.data.attributes;
}
return arguments.data;
}
private function formatErrors(required array errors) {
local.result = [];
for (local.error in arguments.errors) {
ArrayAppend(local.result, {
status: "422",
source: {pointer: "/data/attributes/#local.error.property#"},
title: "Validation Error",
detail: local.error.message
});
}
return local.result;
}
}
API Routes
Generated in /config/routes.cfm
:
<cfset namespace("api")>
<cfset namespace("v1")>
<cfset resources(name="products", except="new,edit")>
<!--- Additional API routes --->
<cfset post(pattern="products/[key]/activate", to="products##activate", on="member")>
<cfset get(pattern="products/search", to="products##search", on="collection")>
</cfset>
</cfset>
API Documentation
Would generate OpenAPI/Swagger documentation:
openapi: 3.0.0
info:
title: Products API
version: 1.0.0
paths:
/api/v1/products:
get:
summary: List products
parameters:
- name: page
in: query
schema:
type: integer
- name: perPage
in: query
schema:
type: integer
- name: filter[name]
in: query
schema:
type: string
- name: sort
in: query
schema:
type: string
responses:
200:
description: Success
content:
application/json:
schema:
$ref: '#/components/schemas/ProductList'
post:
summary: Create product
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ProductInput'
responses:
201:
description: Created
422:
description: Validation error
Workaround Implementation
Until the command is fixed, implement API resources manually:
1. Generate Model
wheels generate model product name:string price:float description:text
2. Create API Controller
Create /controllers/api/v1/Products.cfc
manually with the code above.
3. Add Routes
<!--- In /config/routes.cfm --->
<cfset namespace("api")>
<cfset namespace("v1")>
<cfset resources(name="products", except="new,edit")>
</cfset>
</cfset>
4. Create Tests
wheels generate test controller api/v1/products
API Features
Authentication
// Bearer token authentication
private function authenticate() {
local.token = GetHttpRequestData().headers["Authorization"] ?: "";
local.token = ReReplace(local.token, "^Bearer\s+", "");
if (!Len(local.token) || !isValidToken(local.token)) {
renderWith(
data={error: "Unauthorized"},
status=401
);
}
}
Rate Limiting
// In controller init()
filters(through="rateLimit");
private function rateLimit() {
local.key = "api_rate_limit_" & request.remoteAddress;
local.limit = 100; // requests per hour
if (!StructKeyExists(application, local.key)) {
application[local.key] = {
count: 0,
reset: DateAdd("h", 1, Now())
};
}
if (application[local.key].reset < Now()) {
application[local.key] = {
count: 0,
reset: DateAdd("h", 1, Now())
};
}
application[local.key].count++;
if (application[local.key].count > local.limit) {
renderWith(
data={error: "Rate limit exceeded"},
status=429,
headers={
"X-RateLimit-Limit": local.limit,
"X-RateLimit-Remaining": 0,
"X-RateLimit-Reset": DateTimeFormat(application[local.key].reset, "iso")
}
);
}
}
CORS Headers
// In controller init()
filters(through="setCorsHeaders");
private function setCorsHeaders() {
header name="Access-Control-Allow-Origin" value="*";
header name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS";
header name="Access-Control-Allow-Headers" value="Content-Type, Authorization";
if (request.method == "OPTIONS") {
renderWith(data={}, status=200);
}
}
Testing API Resources
Integration Tests
component extends="wheels.Test" {
function setup() {
super.setup();
model("Product").deleteAll();
}
function test_get_products_returns_json() {
products = createProducts(3);
result = $request(
route="apiV1Products",
method="GET",
headers={"Accept": "application/json"}
);
assert(result.status == 200);
data = DeserializeJSON(result.body);
assert(ArrayLen(data.data) == 3);
assert(data.meta.pagination.total == 3);
}
function test_create_product_with_valid_data() {
params = {
data: {
type: "products",
attributes: {
name: "Test Product",
price: 29.99,
description: "Test description"
}
}
};
result = $request(
route="apiV1Products",
method="POST",
params=params,
headers={
"Content-Type": "application/json",
"Accept": "application/json"
}
);
assert(result.status == 201);
assert(StructKeyExists(result.headers, "Location"));
data = DeserializeJSON(result.body);
assert(data.data.attributes.name == "Test Product");
}
function test_authentication_required() {
result = $request(
route="apiV1Products",
method="POST",
params={},
headers={"Accept": "application/json"}
);
assert(result.status == 401);
}
}
Best Practices
- Version your API: Use URL versioning (/api/v1/)
- Use consistent formats: JSON API or custom format
- Include pagination: Limit response sizes
- Add filtering: Allow query parameters
- Implement sorting: Support field sorting
- Handle errors consistently: Standard error format
- Document thoroughly: OpenAPI/Swagger specs
- Add authentication: Secure endpoints
- Rate limit: Prevent abuse
- Test extensively: Integration tests
See Also
- wheels generate resource - Generate full resources
- wheels generate controller - Generate controllers
- wheels generate model - Generate models
- wheels generate route - Generate routes
- Synopsis
- Current Status
- Arguments (When Enabled)
- Options (When Enabled)
- Intended Functionality
- Basic API Resource
- Generated API Controller
- API Routes
- API Documentation
- Workaround Implementation
- 1. Generate Model
- 2. Create API Controller
- 3. Add Routes
- 4. Create Tests
- API Features
- Authentication
- Rate Limiting
- CORS Headers
- Testing API Resources
- Integration Tests
- Best Practices
- See Also