← Back to Home

Templates & Template Engines

Three template engines for every use case — from simple substitution to fully programmatic generation

Overview

Clay supports three template engines, selectable per generator step via the optional engine field. All engines have access to Clay's 47+ built-in helpers for string manipulation, logic, and iteration.

Engine Best For Syntax
Handlebars (default) Simple substitution, iteration, conditionals {{pascalCase name}}
EJS Templates needing inline JavaScript logic <%= helpers.pascalCase(name) %>
TypeScript Complex generation: cross-references, unique imports, graph traversal CodeGenerator class

Choosing an Engine

Generator Step Configuration

Use the engine field on generate steps to select the engine. The generated file is named after the template's own filename (rendered as a Handlebars template); target is an optional output subdirectory, not the filename. Name the template with the final extension and the per-entity name — don't add .hbs/.ejs (it is stripped from the output name):

{ "generate": "{{pascalCase name}}.ts", "select": "$.model.types[*]" }
{ "generate": "{{pascalCase name}}Service.ts", "select": "$.model.types[*]", "engine": "ejs" }
{ "generate": "index.ts", "select": "$.model", "target": "routes/", "engine": "ts" }

Handlebars Templates

Basic Syntax

Templates use double curly braces to output values:

// {{name}} Model
class {{pascalCase name}} {
  constructor() {
    this.id = null;
  }
}

Context Variables

Clay automatically adds special context variables to help you navigate your model:

clay_model

Access the complete root model from anywhere in your template:

<!-- Access model name from anywhere -->
<h1>{{clay_model.name}}</h1>

<!-- Count total types -->
<p>Total types: {{clay_model.model.types.length}}</p>

clay_parent

Reference the parent element in the JSON structure:

<p>Parent name: {{clay_parent.name}}</p>
<p>Parent path: {{clay_parent.json_path}}</p>

clay_key

The JSON property name of the current element:

<p>Current property: {{clay_key}}</p>

String Helpers

Clay provides extensive string manipulation helpers:

Case Conversion

Helper Example Output
camelCase {{camelCase "user name"}} userName
pascalCase {{pascalCase "user name"}} UserName
kebabCase {{kebabCase "userName"}} user-name
snakeCase {{snakeCase "userName"}} user_name
upperCase {{upperCase "hello"}} HELLO
lowerCase {{lowerCase "HELLO"}} hello
capitalize {{capitalize "hello world"}} Hello world
startCase {{startCase "hello_world"}} Hello World

Pluralization

Helper Example Output
pluralize {{pluralize "category"}} categories
singularize {{singularize "users"}} user

String Utilities

Helper Description Example
pad Pads string to length with spaces {{pad "abc" 5}} → " abc "
repeat Repeats string N times {{repeat "*" 3}} → ***
replace Replaces part of string {{replace "hi" "i" "o"}} → ho
truncate Truncates string to length {{truncate "hello world" 8}} → hello...
words Splits string into word array {{#each (words "foo bar")}}{{this}}{{/each}}
split Splits string by delimiter {{#each (split "a,b,c" ",")}}{{this}}{{/each}}

Comparison Helpers

Compare values in conditional expressions:

Helper Description Example
eq Equals (===) {{#if (eq status "active")}}Active{{/if}}
ne Not equals (!==) {{#if (ne count 0)}}Has items{{/if}}
lt Less than (<) {{#if (lt age 18)}}Minor{{/if}}
gt Greater than (>) {{#if (gt score 100)}}High{{/if}}
lte Less than or equal (<=) {{#if (lte count 10)}}Small{{/if}}
gte Greater than or equal (>=) {{#if (gte age 18)}}Adult{{/if}}

Logic Helpers

Conditionals

{{#if isActive}}
  Active user
{{else}}
  Inactive user
{{/if}}

{{#ifCond age '>=' 18}}
  Adult
{{else}}
  Minor
{{/ifCond}}

The ifCond helper supports these operators:

Logical Operators

{{#if (and isActive isPremium)}}
  Premium Active User
{{/if}}

{{#if (or isAdmin isModerator)}}
  Staff Member
{{/if}}

Switch/Case

{{#switch type}}
  {{#case 'admin'}}
    Administrator
  {{/case}}
  {{#case 'user'}}
    Regular User
  {{/case}}
  {{#default}}
    Unknown
  {{/default}}
{{/switch}}

You can match multiple values in a single case:

{{#switch role}}
  {{#case 'admin' 'moderator'}}
    Staff Member
  {{/case}}
  {{#case 'user'}}
    Regular User
  {{/case}}
{{/switch}}

Property Checking

Use propertyExists to check if a property exists in any object:

{{#if (propertyExists items "email")}}
  <th>Email</th>
{{/if}}

Iteration Helpers

Basic Each

{{#each users}}
  - {{name}} ({{email}})
{{/each}}

Each with Unique Values

Iterate over unique values or unique values by property:

{{!-- Unique values --}}
{{#eachUnique tags}}
  <span>{{this}}</span>
{{/eachUnique}}

{{!-- Unique by property --}}
{{#eachUnique items 'category'}}
  Category: {{this.category}}
{{/eachUnique}}

Each Unique JSONPath

Iterate over items selected by JSONPath (uniqueness determined by path, not value):

{{!-- Select array items (each has unique index in path) --}}
{{#eachUniqueJSONPath clay_model '$.types[*]'}}
  <h2>{{this.name}} - {{this.category}}</h2>
{{/eachUniqueJSONPath}}

Times (Repeat N times)

{{#times 3}}
  Step {{inc @index}}
{{/times}}

Group By

Group items by a property:

{{#group posts by='category'}}
  <h2>{{value}}</h2>
  {{#each items}}
    <p>{{title}}</p>
  {{/each}}
{{/group}}

Type Check Helpers

Check the type of values in your templates:

Helper Description Example
isArray Check if value is an array {{#if (isArray items)}}Is array{{/if}}
isString Check if value is a string {{#if (isString name)}}Is string{{/if}}
isNumber Check if value is a number {{#if (isNumber count)}}Is number{{/if}}
isBoolean Check if value is a boolean {{#if (isBoolean active)}}Is bool{{/if}}
isEmpty Check if value is empty {{#if (isEmpty list)}}No items{{/if}}
isNull Check if value is null {{#if (isNull value)}}Is null{{/if}}
isUndefined Check if value is undefined {{#if (isUndefined opt)}}Not set{{/if}}

Utility Helpers

JSON Output

Pretty-print objects as JSON:

<pre>{{{json this}}}</pre>

Markdown Rendering

Convert markdown to HTML:

{{{markdown description}}}

Number Operations

{{!-- Increment (useful for 1-based indexes) --}}
Step {{inc @index}}

{{!-- Parse string to integer --}}
{{parseInt "42"}}

String Checks

{{!-- Check if string contains value --}}
{{#if (includes email "@")}}Valid email format{{/if}}

{{!-- Check if string starts with value --}}
{{#if (startsWith filename "test_")}}Is test file{{/if}}

{{!-- Check if string ends with value --}}
{{#if (endsWith filename ".js")}}Is JS file{{/if}}

Split and Extract

Split strings by delimiter and extract parts:

{{!-- Using split helper --}}
{{#each (split "foo,bar,baz" ",")}}
  - {{this}}
{{/each}}

{{!-- Using splitAndUseWord to get specific part --}}
{{splitAndUseWord "user-profile-page" "-" 1}}
{{!-- Output: profile --}}

Partials

Reuse template fragments with partials. Define them in your generator:

{
  "partials": ["partials/header.hbs"],
  "steps": [...]
}

Use in templates:

{{> header}}
<div class="content">
  {{name}}
</div>

Complete Helper List

Clay includes 47+ helpers across 8 categories:

💡 Pro Tip: Use the MCP server's clay_list_helpers tool to get detailed examples of all helpers when working with AI assistants!

EJS Templates

EJS templates embed JavaScript directly in the template using <% %> tags. Use EJS when you need a few lines of computation inside an otherwise template-like file.

Basic Syntax

<% // JavaScript code (no output) %>
<%= expression %>   <!-- Output (escaped) -->
<%- expression %>   <!-- Output (unescaped) -->

Example: Entity with Computed Imports

<% const uniqueRefs = [...new Set(
  (fields || []).filter(f => f.type === 'reference').map(f => f.reference)
)]; %>
<% uniqueRefs.forEach(ref => { %>
import { <%= helpers.pascalCase(ref) %> } from './<%= helpers.pascalCase(ref) %>';
<% }); %>

export class <%= helpers.pascalCase(name) %> {
<% (fields || []).forEach(f => { %>
  <%= helpers.camelCase(f.name) %>: <%= f.type %>;
<% }); %>
}

Accessing Helpers

In EJS templates, Clay helpers are available via the helpers object:

<%= helpers.pascalCase(name) %>
<%= helpers.pluralize(name) %>
<%= helpers.camelCase(fieldName) %>

TypeScript Templates (CodeGenerator)

For fully programmatic generation, use the CodeGenerator base class. TypeScript templates are ideal for complex wiring files, DI containers, route registrations, and any file where the generation logic is more code than template.

Basic Structure

import { CodeGenerator, type RenderContext } from 'clay-generator/types';

export default class extends CodeGenerator {
  render({ data, helpers, model }: RenderContext): string {
    const { pascalCase, camelCase } = helpers;
    return `export class ${pascalCase(data.name)} {}`;
  }
}

RenderContext

The render() method receives a RenderContext with all available data:

Property Type Description
data Record<string, any> The selected model item (e.g., one entity from $.model.types[*])
helpers ClayHelpers All Clay helpers — pascalCase, camelCase, pluralize, etc.
model Record<string, any> The full root model (equivalent to clay_model in Handlebars)
parent Record<string, any> Parent object in the JSON hierarchy (equivalent to clay_parent)

Async Render

The render() method can be async. This enables database queries, API calls, or file reads during generation:

import { CodeGenerator, type RenderContext } from 'clay-generator/types';

export default class extends CodeGenerator {
  async render({ data, helpers }: RenderContext): Promise<string> {
    // Can do async operations — database queries, API calls, etc.
    return `export class ${helpers.pascalCase(data.name)}Service {}`;
  }
}

Full Example: Route Registration

import { CodeGenerator, type RenderContext } from 'clay-generator/types';

export default class extends CodeGenerator {
  render({ data, helpers }: RenderContext): string {
    const { pascalCase, kebabCase } = helpers;
    const types = data.model.types;

    const imports = types
      .map((t: any) =>
        `import { ${pascalCase(t.name)}Controller } from './${pascalCase(t.name)}Controller';`
      )
      .join('\n');

    const routes = types
      .map((t: any) =>
        `  router.use('/${kebabCase(t.name)}', new ${pascalCase(t.name)}Controller().routes());`
      )
      .join('\n');

    return `${imports}

export function registerRoutes(router: Router) {
${routes}
}`;
  }
}

Filesystem-Aware Generation

TypeScript templates can read the filesystem, making them ideal for barrel files, route registrations, and DI containers that must reflect what actually exists on disk. No stale exports for deleted files, no missing exports for new ones.

import { CodeGenerator, type RenderContext } from 'clay-generator/types';
import fs from 'fs';
import path from 'path';

export default class extends CodeGenerator {
  render({ data, helpers }: RenderContext): string {
    const dir = path.resolve('src/services');
    const files = fs.readdirSync(dir)
      .filter(f => f.endsWith('.ts') && f !== 'index.ts')
      .map(f => f.replace('.ts', ''));

    return files.map(f => `export * from './${f}';`).join('\n');
  }
}

This pattern works for any file that aggregates across a directory:

npm Package Access

TypeScript templates can import any npm package installed in your project. This enables powerful use cases like querying databases or fetching API specs during generation:

import { CodeGenerator, type RenderContext } from 'clay-generator/types';
import { Client } from 'pg';

export default class extends CodeGenerator {
  async render({ data, helpers }: RenderContext): Promise<string> {
    const client = new Client();
    await client.connect();
    const columns = await client.query(
      'SELECT column_name, data_type FROM information_schema.columns WHERE table_name = $1',
      [data.name]
    );
    await client.end();
    // Generate code based on actual database schema
    return `// Generated from database schema for ${data.name}\n...`;
  }
}

Helper Availability Across Engines

Engine How to Access Helpers
Handlebars {{pascalCase name}} — registered as Handlebars helpers
EJS helpers.pascalCase(name) — via helpers object
TypeScript helpers.pascalCase(name) — via RenderContext.helpers

Best Practices