Every backend generator (honoGenerator, h3Generator, koaGenerator) accepts a templates option. When you create a new route file whose name matches one of your patterns, the generator writes your boilerplate into it instead of the built-in placeholder.
This is the backend half of the feature; pages have their own. Custom Page Templates ›
What It Overrides
Only the route file - the index.ts holding your defineRoute definition.
| File | Templatable? |
|---|---|
api/**/index.ts - route definitions | ✅ |
api/**/use.ts - cascading middleware | ❌ always the built-in |
api/app.ts, errors.ts, dev.ts, server.ts, env.d.ts | ❌ deployed once at folder creation |
Templates only fill blank files
The generator writes boilerplate into a file only when that file is empty. It never overwrites work you have already done - which is also why changing a template does not retroactively rewrite existing routes. To re-scaffold one, empty the file and let it be regenerated.
Configuration
Pass templates to your backend generator in the source folder's kosmo.config.ts, keyed by route-name pattern:
import { defineConfig, honoGenerator } from "@kosmojs/dev";
const crudTemplate = `
import { defineRoute } from "_/api";
export default defineRoute<"{{route.name}}">(({ GET, POST, PUT, DELETE }) => [
GET(async (ctx) => { /* list or read */ }),
POST(async (ctx) => { /* create */ }),
PUT(async (ctx) => { /* update */ }),
DELETE(async (ctx) => { /* delete */ }),
]);`;
export default defineConfig({
base: "/",
generators: [
honoGenerator({
templates: {
"admin/**": crudTemplate,
},
}),
],
});Keys match the route name - the path relative to api/, without the trailing index.ts. So api/admin/users/index.ts is matched as admin/users.
Pattern Syntax
Patterns are picomatch globs matched against the route name.
Single-depth wildcard (*)
{ "admin/*": template }Matches: admin/users, admin/settings, admin/[id]Excludes: admin/users/roles (too deep), admin (too shallow)
Multi-depth wildcard (**)
{ "admin/**": template }Matches: admin/users, admin/users/roles, admin/[id]/audit - any depth.
Exact match
{ "health": template }Parameter segments are literal
Route parameters are escaped before matching, so [id], {id} and {...path} mean themselves rather than glob syntax:
{
"users/[id]": userTemplate, // required parameter
"products/{category}": productTemplate, // optional parameter
"docs/{...path}": docsTemplate, // splat parameter
"shop/[category]/{sub}": shopTemplate, // combined
}Resolution Priority
The first matching pattern wins, in the order the keys are written - so order them most specific first:
templates: {
"admin/audit": auditTemplate, // exact
"admin/*": adminTemplate, // one level
"**": fallbackTemplate, // everything else
}A route matching nothing gets the generator's built-in placeholder. Set "**" to replace that default everywhere.
Numeric-looking patterns jump the queue
JavaScript objects order integer-like keys first, regardless of where you wrote them - so a pattern such as "2024/**" is hoisted to the front and matches before anything above it. Prefix it with ./ to keep your written order: "./2024/**". The ./ is stripped before matching.
The same applies to renderMode, which uses the same resolver.
Template Format
A template is either a string, or a function of the resolved route returning a string - reach for the function form when the output depends on the route itself:
templates: {
// plain string, rendered with Handlebars
"admin/**": crudTemplate,
// or computed from the route
"reports/**": (route) => `
import { defineRoute } from "_/api";
// endpoint: ${route.name}
export default defineRoute<"${route.name}">(({ GET }) => [
GET(async (ctx) => { /* ... */ }),
]);`,
}String templates are rendered with Handlebars against a { route } context, so a {{route.name}} placeholder interpolates the route name - which is what keeps the required route-name type argument correct in generated files.
Per-backend shapes
Because the context API differs by backend, so does the boilerplate:
const template = `
import { defineRoute } from "_/api";
export default defineRoute<"{{route.name}}">(({ GET }) => [
GET(async (ctx) => {
// Always \`return\` the response
return ctx.json({ ok: true });
}),
]);`;Escape backticks (
\`) and${...}inside template literals that you do not want interpolated at config-evaluation time.
Scaffolding CRUD Endpoints
This is where route templates earn their place. Standing up endpoints for a dozen database tables means writing the same skeleton a dozen times - validation targets, method handlers, error shape, all identical apart from the type.
Write it once:
const resourceTemplate = `
import { defineRoute } from "_/api";
import type { Resource, ResourcePayload } from "./types";
export default defineRoute<"{{route.name}}">(({ GET, POST }) => [
GET<{
query: { page?: number; limit?: number },
response: [200, "json", Array<Resource>],
}>(async (ctx) => {
// list
}),
POST<{
json: ResourcePayload,
response: [201, "json", Resource],
}>(async (ctx) => {
// create
}),
]);`;
export default defineConfig({
base: "/",
generators: [
honoGenerator({ templates: { "admin/**": resourceTemplate } }),
],
});Then create the route folders. Each new index.ts arrives with the right structure - typed handlers, declared response (so the fetch client is typed and OpenAPI picks it up), ready to adapt instead of retyped.
Pair it with a colocated types.ts per route folder and the skeleton stays honest: the template references ./types, and each route defines its own.
Common Use Cases
honoGenerator({
templates: {
"admin/**": crudTemplate, // consistent admin endpoints
"webhooks/**": webhookTemplate, // signature verification + 200 fast
"internal/**": internalTemplate, // runtimeValidation: false, trusted callers
},
})