Templates
Create dynamic file templates using Handlebars. This example covers everything from simple variable substitution to advanced template patterns.
How templates work
Templates are Handlebars (.hbs) files that the generator renders with context variables. The rendered output is written to the file path you specify.
Template resolution order:
- User templates directory (if configured in your project)
- Built-in templates bundled with the package
Place your custom templates in a directory relative to your project root. The generator looks there first, so you can override any built-in template by creating a file with the same name.
Available variables
Every template receives these variables:
| Variable | Example value | Description |
|---|---|---|
{{PascalName}} | UserProfile | PascalCase feature name |
{{camelName}} | userProfile | camelCase feature name |
{{snakeName}} | user_profile | snake_case feature name |
{{upperName}} | USER_PROFILE | UPPER_CASE feature name |
{{name}} | user-profile | Original kebab-case name |
Example 1: Component template
Template: Component.tsx.hbs
import React from "react"
export interface {{PascalName}}Props {
className?: string
children?: React.ReactNode
}
export const {{PascalName}} = ({ className, children }: {{PascalName}}Props) => {
return (
<div className={className}>
{{PascalName}} component
</div>
)
}
export default {{PascalName}}Generated output (for user-profile):
import React from "react"
export interface UserProfileProps {
className?: string
children?: React.ReactNode
}
export const UserProfile = ({ className, children }: UserProfileProps) => {
return (
<div className={className}>
UserProfile component
</div>
)
}
export default UserProfileExample 2: Hook template with TanStack Query
Template: hooks/useHook.ts.hbs
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"
import { {{PascalName}}Service } from "../services/{{camelName}}Service"
import type { {{PascalName}} } from "../types/{{PascalName}}Types"
export function use{{PascalName}}List() {
return useQuery({
queryKey: ["{{camelName}}"],
queryFn: {{PascalName}}Service.getAll,
})
}
export function use{{PascalName}}(id: string) {
return useQuery({
queryKey: ["{{camelName}}", id],
queryFn: () => {{PascalName}}Service.getById(id),
})
}
export function useCreate{{PascalName}}() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: {{PascalName}}Service.create,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["{{camelName}}"] })
},
})
}
export function useDelete{{PascalName}}() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: {{PascalName}}Service.delete,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["{{camelName}}"] })
},
})
}Example 3: Zustand store template
Template: store/store.ts.hbs
import { create } from "zustand"
import { devtools } from "zustand/middleware"
import type { {{PascalName}} } from "../types/{{PascalName}}Types"
interface {{PascalName}}State {
items: {{PascalName}}[]
selectedItem: {{PascalName}} | null
isLoading: boolean
error: string | null
setItems: (items: {{PascalName}}[]) => void
selectItem: (item: {{PascalName}} | null) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
reset: () => void
}
const initialState = {
items: [],
selectedItem: null,
isLoading: false,
error: null,
}
export const use{{PascalName}}Store = create<{{PascalName}}State>()(
devtools(
(set) => ({
...initialState,
setItems: (items) => set({ items }),
selectItem: (item) => set({ selectedItem: item }),
setLoading: (isLoading) => set({ isLoading }),
setError: (error) => set({ error }),
reset: () => set(initialState),
}),
{ name: "{{PascalName}}Store" }
)
)Example 4: Zod validation template
Template: validations/schema.ts.hbs
import { z } from "zod"
export const {{PascalName}}Schema = z.object({
id: z.string().uuid().optional(),
name: z
.string()
.min(1, "Name is required")
.max(100, "Name must be 100 characters or less"),
description: z
.string()
.max(500, "Description must be 500 characters or less")
.optional(),
status: z.enum(["active", "inactive", "draft"]).default("draft"),
createdAt: z.string().datetime().optional(),
updatedAt: z.string().datetime().optional(),
})
export type {{PascalName}}FormData = z.infer<typeof {{PascalName}}Schema>Example 5: Conditional content with Handlebars helpers
Handlebars supports basic conditionals and loops. While the built-in templates keep it simple, here are patterns you can use:
Conditional export:
{{#if isDefaultExport}}
export default {{PascalName}}
{{/if}}Loop over custom items:
export const {{PascalName}}Fields = [
{{#each fields}}
{ name: "{{this}}", type: "string" },
{{/each}}
]Custom context merging:
The context object is extensible — you can pass additional variables when calling the API programmatically:
await generateFeature("user-profile", config, {
context: {
isDefaultExport: true,
fields: ["name", "email", "role"],
},
})Template file location
Templates are looked up in this order:
- User templates directory — Looked up relative to the
templatespath in your config - Built-in templates — Bundled with the package, used as fallback
To override a built-in template, create a file with the same path in your project. For example, to override the React component template, create:
your-project/
├── templates/
│ └── components/
│ └── Component.tsx.hbsUsing EJS instead of Handlebars
Optionally use EJS templates. Install the dependency:
npm install ejsThen register the engine:
import { registerTemplateEngine } from "@heco-gen/features"
import ejs from "ejs"
registerTemplateEngine("ejs", async (template, context) => {
return ejs.render(template, context)
})Now templates with .ejs extension will use the EJS engine instead of Handlebars.
Organizing templates
your-project/
├── templates/
│ ├── components/
│ │ └── Component.tsx.hbs
│ ├── hooks/
│ │ └── useHook.ts.hbs
│ ├── services/
│ │ └── service.ts.hbs
│ ├── types/
│ │ └── types.ts.hbs
│ ├── validations/
│ │ └── schema.ts.hbs
│ └── store/
│ └── store.ts.hbsThis mirrors the output structure and keeps templates organized.
Next steps
- Custom Generators — combine templates with custom file structures
- Programmatic Usage — pass custom context variables programmatically