---
url: /backend/custom-templates.md
description: >-
  Override the seeded defineRoute boilerplate for specific API routes using glob
  pattern matching. Seed consistent CRUD endpoints across many routes in Hono,
  H3 and Koa.
---

Every backend generator accepts a `templates` option.
When you create a new route file whose name matches one of your patterns,
**your** boilerplate is written into it instead of the built-in placeholder.

Templates seed route `index` files - `api/**/index.ts` - and nothing else.
Every other seeded file gets the built-in minimal boilerplate to start from.

## Configuration

Pass `templates` through the `backend` block in the source folder's `kosmo.config.ts`,
keyed by route-name pattern:

```ts [kosmo.config.ts]
import { defineConfig } 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({
  backend: {
    stack: "hono",
    base: "/api",
    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](https://github.com/micromatch/picomatch) globs matched against the route name.

### Single-depth wildcard (`*`)

```ts
{ "admin/*": template }
```

**Matches:** `admin/users`, `admin/settings`, `admin/[id]`
**Excludes:** `admin/users/roles` (too deep), `admin` (too shallow)

### Multi-depth wildcard (`**`)

```ts
{ "admin/**": template }
```

**Matches:** `admin/users`, `admin/users/roles`, `admin/[id]/audit` - any depth.

### Exact match

```ts
{ "health": template }
```

### Parameter segments are literal

Route parameters are escaped before matching, so `[id]`, `{id}` and `{...path}` mean
themselves rather than glob syntax:

```ts
{
  "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:

```ts
templates: {
  "admin/audit": auditTemplate,   // exact
  "admin/*": adminTemplate,       // one level
  "**": fallbackTemplate,         // everything else
}
```

A route matching nothing gets the built-in placeholder.
Set `"**"` to replace that default everywhere.

::: warning 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](/frontend/server-side-render.md#selecting-the-render-mode), 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:

```ts
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](/backend/intro.md#the-route-name-type-argument) correct in seeded files.

### Per-backend shapes

Because the context API differs by backend, so does the boilerplate:

#### Custom Templates | Per-backend shapes | Hono

```ts
// Hono: kosmo.config.ts
const template = `
import { defineRoute } from "_/api";

export default defineRoute<"{{route.name}}">(({ GET }) => [
  GET(async (ctx) => {
    // Always \`return\` the response
    return ctx.json({ ok: true });
  }),
]);`;
```

#### Custom Templates | Per-backend shapes | H3

```ts
// H3: kosmo.config.ts
const template = `
import { defineRoute } from "_/api";

export default defineRoute<"{{route.name}}">(({ GET }) => [
  GET(async (event) => {
    // return the value directly - objects serialize as JSON
    return { ok: true };
  }),
]);`;
```

#### Custom Templates | Per-backend shapes | Koa

```ts
// Koa: kosmo.config.ts
const template = `
import { defineRoute } from "_/api";

export default defineRoute<"{{route.name}}">(({ GET }) => [
  GET(async (ctx) => {
    ctx.body = { ok: true };
  }),
]);`;
```

> Escape backticks (`` \` ``) and `${...}` inside template literals
> that you do not want interpolated at config-evaluation time.

## Seeding 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:

```ts [kosmo.config.ts]
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({
  backend: {
    stack: "hono",
    base: "/api",
    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](/fetch/type-safety.md#without-a-response-the-result-is-unknown) 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

```ts
backend: {
  stack: "hono",
  base: "/api",
  templates: {
    "admin/**": crudTemplate,        // consistent admin endpoints
    "webhooks/**": webhookTemplate,  // signature verification + 200 fast
    "internal/**": internalTemplate, // runtimeValidation: false, trusted callers
  },
}
```
