Skip to content

Core Concepts

Conventions over Configuration

Wheels trades explicit configuration for naming conventions. A file in the right place with the right name wires itself into the framework without a line of setup — and when a convention doesn’t fit your schema, you override it in one line and move on.

You’ll learn:

  • What “convention over configuration” means in practice
  • Which conventions Wheels enforces and what each one infers
  • What breaking a convention costs
  • When an override is the right call

Naming is the glue between layers. The framework reads the name of a file, class, action, or association and fills in the rest — table name, view path, routes, foreign keys — without any registration step. You get a working surface area from the filename alone.

You createWheels infers
app/models/Post.cfcModel class Post, table posts, primary key id, datasource = app default
app/controllers/Posts.cfcController Posts, view dir app/views/posts/, routes via .resources("posts")
Action index on PostsView path app/views/posts/index.cfm, rendered through app/views/layout.cfm
config/routes.cfm with .resources("posts")Seven routes (posts/newPost/post/editPost named), CRUD mapping
hasMany(name="comments") on PostForeign key postId on comments; methods post.comments() and post.createComment()

Every row in that table is a decision you didn’t have to make. No registry file lists your models. No manifest maps actions to views. No config block wires a controller to a route. Rename a file and the wiring follows. Delete a file and the wiring is gone.

The rules themselves are narrow and mechanical. Model classes are singular PascalCase; table names are the plural, lowercase form of the class name. Controller classes are plural PascalCase; the view directory is the lowercase form of the controller name. Primary keys are id. Foreign keys are the singular association name plus IdpostId for a belongsTo("post"), userId for a belongsTo("user"). Every rule follows the same shape: given a name, there is exactly one location the framework will look, and exactly one shape it expects.

Every convention has a one-line escape hatch. The overrides live where the convention would have applied — in the model’s config(), in the route definition, in the renderView() call. You don’t reach into framework internals to change the mapping; you declare the exception next to the thing it applies to.

  • Custom table: tableName("old_blog_entries") inside the model’s config().
  • Custom primary key: setPrimaryKey("entryId") inside config(). Composite keys pass an array.
  • Custom datasource: dataSource("legacy") inside config() picks a non-default datasource for this model.
  • Custom foreign key on an association: hasMany(name="comments", foreignKey="blogPostId") overrides the postId default.
  • Custom view path: renderView(action="summary") or renderView(controller="shared", action="error") picks a different template without renaming the action.
  • Custom layout: renderView(layout="print") for one action, or a filters(through="$usePrintLayout") filter that calls usesLayout("print") across a controller.
  • Custom route mapping: .get(pattern="/old/:key", to="posts##show") in config/routes.cfm maps a legacy URL onto a conventional action.

Each override is local. The rest of the model, the rest of the controller, the rest of the app keeps the default behavior. You pay the configuration cost only where the convention didn’t fit.

The same shape holds for every override in the framework. The convention fires by default; a single named argument or a single line in config() replaces it with a value you supply. You don’t disable conventions globally, and there’s no flag that turns them off. The surface area stays predictable because every exception is visible next to the thing that needed it.

Legacy databases are the usual source of friction. Tables named tbl_user_data, primary keys called userID instead of id, pluralizations Wheels’ inflector doesn’t recognize (person/people, datum/data, goose/geese), columns that carry JSON or enum values on a schema designed before either was a standard type. Multi-tenancy with per-tenant schemas or table prefixes. Foreign keys that don’t follow the <model>Id pattern because someone named them after a product code twenty years ago.

None of these block you. Each has a declared override — tableName(), setPrimaryKey(), foreignKey=, a custom inflector rule, a dynamic scope, a middleware that resolves the tenant. The cost is a handful of extra lines in the model and a reader who has to know the schema is unusual. That’s a real cost, but it’s paid once per table, not once per query. Most apps have five or ten such exceptions and several hundred conventional models.

The pattern to avoid is configuring away a convention you could have followed. If a new table is going in and the name is negotiable, pick the plural lowercase form the framework expects. If a new foreign key is going in, name it <model>Id. The conventions cost nothing when you adopt them and compound every time you add a model, an association, or a controller. The exceptions are for the schemas you inherited, not the schemas you write.

Conventions reduce the decision surface. A developer opening a Wheels app for the first time already knows where models, controllers, views, migrations, and routes live — the directory structure is the same in every Wheels app. They know that Post.cfc maps to a posts table without reading your configuration. They know the show action renders posts/show.cfm. The part of the codebase they have to read to understand what’s happening is the part you actually wrote, not the scaffolding around it.

Tooling depends on the same assumptions. Generators produce files at the expected paths. Scaffolds wire controllers to views to routes because the routes and views are where the conventions say they are. The wheels CLI, migrations, seeds, and test runners all rely on the convention to locate the things they operate on. Breaking a convention shifts cost from the framework to your app: the tools stop helping, and every new developer has to learn one more thing that isn’t in the docs. Override where you must, follow the convention where you can.