wheels generate resource
Generate a complete RESTful resource with model, controller, views, and routes.
Synopsis
wheels generate resource [name] [properties] [options]
wheels g resource [name] [properties] [options]
Description
The wheels generate resource
command creates a complete RESTful resource including model, controller with all CRUD actions, views, routes, and optionally database migrations and tests. It's a comprehensive generator that sets up everything needed for a functioning resource.
Arguments
name
Resource name (singular)
Required
Options
--api
Generate API-only resource (no views)
false
--tests
Generate associated tests
true
--migration
Generate database migration
true
belongs-to
Parent model relationships (comma-separated)
has-many
Child model relationships (comma-separated)
attributes
Model attributes (name:type,email:string)
--open
Open generated files
false
--scaffold
Generate with full CRUD operations
true
Examples
Basic Resource
wheels generate resource product attributes="name:string,price:float,description:text"
Generates:
Model:
/models/Product.cfc
Controller:
/controllers/Products.cfc
Views:
/views/products/
(index, show, new, edit, _form)Route:
resources("products")
in/config/routes.cfm
Migration:
/app/migrator/migrations/[timestamp]_create_products.cfc
Tests:
/tests/models/ProductTest.cfc
,/tests/controllers/ProductsTest.cfc
API Resource
wheels generate resource product attributes="name:string,price:float" --api
Generates:
Model:
/models/Product.cfc
Controller:
/controllers/api/Products.cfc
(JSON responses only)Route:
resources(name="products", except="new,edit")
in API namespaceMigration:
/app/migrator/migrations/[timestamp]_create_products.cfc
Tests: API-focused test files
Resource with Associations
wheels generate resource comment attributes="content:text,approved:boolean" belongs-to="post,user"
Generates nested structure with proper associations and routing.
Generated Files
Model
/models/Product.cfc
:
component extends="Model" {
function init() {
// Properties
property(name="name", sql="name");
property(name="price", sql="price");
property(name="description", sql="description");
// Validations
validatesPresenceOf(properties="name,price");
validatesNumericalityOf(property="price", greaterThan=0);
validatesLengthOf(property="name", maximum=255);
// Callbacks
beforeSave("sanitizeInput");
}
private function sanitizeInput() {
this.name = Trim(this.name);
if (StructKeyExists(this, "description")) {
this.description = Trim(this.description);
}
}
}
Controller
/controllers/Products.cfc
:
component extends="Controller" {
function init() {
// Filters
filters(through="findProduct", only="show,edit,update,delete");
}
function index() {
products = model("Product").findAll(order="createdAt DESC");
}
function show() {
// Product loaded by filter
}
function new() {
product = model("Product").new();
}
function create() {
product = model("Product").new(params.product);
if (product.save()) {
flashInsert(success="Product was created successfully.");
redirectTo(route="product", key=product.id);
} else {
renderView(action="new");
}
}
function edit() {
// Product loaded by filter
}
function update() {
if (product.update(params.product)) {
flashInsert(success="Product was updated successfully.");
redirectTo(route="product", key=product.id);
} else {
renderView(action="edit");
}
}
function delete() {
if (product.delete()) {
flashInsert(success="Product was deleted successfully.");
} else {
flashInsert(error="Product could not be deleted.");
}
redirectTo(route="products");
}
// Filters
private function findProduct() {
product = model("Product").findByKey(params.key);
if (!IsObject(product)) {
flashInsert(error="Product not found.");
redirectTo(route="products");
}
}
}
Views
/views/products/index.cfm
:
<h1>Products</h1>
<p>
#linkTo(route="newProduct", text="New Product", class="btn btn-primary")#
</p>
<cfif products.recordCount>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Price</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<cfoutput query="products">
<tr>
<td>#linkTo(route="product", key=products.id, text=products.name)#</td>
<td>#dollarFormat(products.price)#</td>
<td>#dateFormat(products.createdAt, "mmm dd, yyyy")#</td>
<td>
#linkTo(route="editProduct", key=products.id, text="Edit", class="btn btn-sm btn-secondary")#
#linkTo(route="product", key=products.id, text="Delete", method="delete", confirm="Are you sure?", class="btn btn-sm btn-danger")#
</td>
</tr>
</cfoutput>
</tbody>
</table>
<cfelse>
<p class="alert alert-info">No products found. #linkTo(route="newProduct", text="Create one now")#!</p>
</cfif>
/views/products/_form.cfm
:
#errorMessagesFor("product")#
#startFormTag(route=formRoute, key=formKey, class="needs-validation")#
<div class="mb-3">
#textField(objectName="product", property="name", label="Product Name", class="form-control", required=true)#
</div>
<div class="mb-3">
#numberField(objectName="product", property="price", label="Price", class="form-control", step="0.01", min="0", required=true)#
</div>
<div class="mb-3">
#textArea(objectName="product", property="description", label="Description", class="form-control", rows=5)#
</div>
<div class="mb-3">
#submitTag(value=submitValue, class="btn btn-primary")#
#linkTo(route="products", text="Cancel", class="btn btn-secondary")#
</div>
#endFormTag()#
Migration
/app/migrator/migrations/[timestamp]_create_products.cfc
:
component extends="wheels.migrator.Migration" hint="Create products table" {
function up() {
transaction {
createTable(name="products", force=true) {
t.increments("id");
t.string("name", limit=255, null=false);
t.decimal("price", precision=10, scale=2, null=false);
t.text("description");
t.timestamps();
t.index("name");
};
}
}
function down() {
transaction {
dropTable("products");
}
}
}
Routes
Added to /config/routes.cfm
:
<cfset resources("products")>
API Resource Generation
API Controller
/controllers/api/Products.cfc
:
component extends="Controller" {
function init() {
provides("json");
filters(through="findProduct", only="show,update,delete");
}
function index() {
products = model("Product").findAll(
order="createdAt DESC",
page=params.page ?: 1,
perPage=params.perPage ?: 25
);
renderWith({
data: products,
meta: {
page: products.currentPage,
totalPages: products.totalPages,
totalRecords: products.totalRecords
}
});
}
function show() {
renderWith(product);
}
function create() {
product = model("Product").new(params.product);
if (product.save()) {
renderWith(data=product, status=201);
} else {
renderWith(
data={errors: product.allErrors()},
status=422
);
}
}
function update() {
if (product.update(params.product)) {
renderWith(product);
} else {
renderWith(
data={errors: product.allErrors()},
status=422
);
}
}
function delete() {
if (product.delete()) {
renderWith(data={message: "Product deleted successfully"});
} else {
renderWith(
data={error: "Could not delete product"},
status=400
);
}
}
private function findProduct() {
product = model("Product").findByKey(params.key);
if (!IsObject(product)) {
renderWith(
data={error: "Product not found"},
status=404
);
}
}
}
Nested Resources
Generate Nested Resource
wheels generate resource review rating:integer comment:text parent=product
Nested Model
Includes association:
component extends="Model" {
function init() {
belongsTo("product");
property(name="rating", sql="rating");
property(name="comment", sql="comment");
property(name="productId", sql="product_id");
validatesPresenceOf(properties="rating,comment,productId");
validatesNumericalityOf(property="rating", greaterThanOrEqualTo=1, lessThanOrEqualTo=5);
}
}
Nested Routes
<cfset resources("products")>
<cfset resources("reviews")>
</cfset>
Property Types
Supported Types
string
VARCHAR(255)
Length validation
text
TEXT
None by default
integer
INT
Numerical validation
float
DECIMAL
Numerical validation
decimal
DECIMAL
Numerical validation
boolean
BOOLEAN
Boolean validation
date
DATE
Date format validation
datetime
DATETIME
DateTime validation
time
TIME
Time validation
binary
BLOB
None
Property Options
wheels generate resource user \
email:string:required:unique \
age:integer:min=18:max=120 \
bio:text:limit=1000 \
isActive:boolean:default=true
Advanced Options
Skip Components
# Generate only model and migration
wheels generate resource product name:string --skip-controller --skip-views --skip-route
# Generate only controller and views
wheels generate resource product --skip-model --skip-migration
Namespace Resources
wheels generate resource admin/product name:string namespace=admin
Creates:
/controllers/admin/Products.cfc
/views/admin/products/
Namespaced routes
Custom Templates
wheels generate resource product name:string template=custom
Testing
Generated Tests
Model Test (/tests/models/ProductTest.cfc
):
component extends="wheels.Test" {
function setup() {
super.setup();
model("Product").deleteAll();
}
function test_valid_product_saves() {
product = model("Product").new(
name="Test Product",
price=19.99,
description="Test description"
);
assert(product.save());
assert(product.id > 0);
}
function test_requires_name() {
product = model("Product").new(price=19.99);
assert(!product.save());
assert(ArrayLen(product.errorsOn("name")) > 0);
}
function test_requires_positive_price() {
product = model("Product").new(name="Test", price=-10);
assert(!product.save());
assert(ArrayLen(product.errorsOn("price")) > 0);
}
}
Controller Test (/tests/controllers/ProductsTest.cfc
):
component extends="wheels.Test" {
function test_index_returns_products() {
products = createProducts(3);
result = processRequest(route="products", method="GET");
assert(result.status == 200);
assert(Find("Products", result.body));
assert(FindNoCase(products[1].name, result.body));
}
function test_create_valid_product() {
params = {
product: {
name: "New Product",
price: 29.99,
description: "New product description"
}
};
result = processRequest(route="products", method="POST", params=params);
assert(result.status == 302);
assert(model("Product").count() == 1);
}
}
Best Practices
Use singular names:
product
notproducts
Define all properties: Include types and validations
Add indexes: For frequently queried fields
Include tests: Don't skip test generation
Use namespaces: For admin or API resources
Follow conventions: Stick to RESTful patterns
Common Patterns
Soft Delete Resource
wheels generate resource product name:string deletedAt:datetime:nullable
Publishable Resource
wheels generate resource post title:string content:text publishedAt:datetime:nullable status:string:default=draft
User-Owned Resource
wheels generate resource task title:string userId:integer:belongsTo=user completed:boolean:default=false
Hierarchical Resource
wheels generate resource category name:string parentId:integer:nullable:belongsTo=category
Customization
Custom Resource Templates
Create in ~/.wheels/templates/resources/
:
custom-resource/
├── model.cfc
├── controller.cfc
├── views/
│ ├── index.cfm
│ ├── show.cfm
│ ├── new.cfm
│ ├── edit.cfm
│ └── _form.cfm
└── migration.cfc
Template Variables
Available in templates:
${resourceName}
- Singular name${resourceNamePlural}
- Plural name${modelName}
- Model class name${controllerName}
- Controller class name${tableName}
- Database table name${properties}
- Array of property definitions
See Also
wheels scaffold - Interactive CRUD generation
wheels generate model - Generate models only
wheels generate controller - Generate controllers only
wheels generate api-resource - Generate API resources
wheels generate route - Generate routes only
Last updated
Was this helpful?