Skip to main content

Design the workflow

A Skaff template is a directory with a TypeScript config file and a set of files to render. Keep the layout predictable so contributors can find everything quickly.

Directory layout

root-templates/
service-template/
templateConfig.ts
templates/
package.json.hbs
src/index.ts.hbs
subtemplates/
auth/
templateConfig.ts
templates/
  • templateConfig.ts exports a TemplateConfigModule. Define metadata (name, description, specVersion), the Zod schemas for user input (templateSettingsSchema and templateFinalSettingsSchema), and the mapFinalSettings function that produces the values used during rendering. Set targetPath so Skaff knows where files land.
  • templates/ holds files to render. Append .hbs to process a file with Handlebars; omit it for plain copies. Skaff copies the directory structure relative to targetPath.
  • subtemplates/ hosts optional modules. Structure them the same way as root templates so they can run independently.

Model user input

Use Zod to describe every prompt. Each field becomes a CLI question and a form control in the web app. Provide helpful descriptions so teams understand how values are used.

templateConfig.ts
import z from "zod";
import type {
FinalTemplateSettings,
TemplateConfigModule,
} from "@timonteutelink/template-types-lib";

const templateSettingsSchema = z.object({
projectName: z.string().min(1).describe("NPM package name"),
includeAuth: z
.boolean()
.default(false)
.describe("Generate auth routes and providers"),
});

type TemplateSettings = z.infer<typeof templateSettingsSchema>;

const templateConfig: TemplateConfigModule<
FinalTemplateSettings,
typeof templateSettingsSchema,
typeof templateSettingsSchema
> = {
templateConfig: {
name: "service-template",
author: "Example team",
description: "Production-ready service starter",
specVersion: "1.0.0",
},
templateSettingsSchema,
templateFinalSettingsSchema: templateSettingsSchema,
mapFinalSettings: ({ templateSettings }): TemplateSettings =>
templateSettings,
targetPath: ".",
};

export default templateConfig;

Adjust mapFinalSettings whenever you need to derive additional values—such as generated slugs, computed booleans, or data pulled from the parent settings.

Wire up subtemplates

Subtemplates keep optional features separate:

  • Use autoInstantiatedSubtemplates in the parent to include children automatically based on settings.
  • Mark subtemplates as multiInstance when they can be reused (for example, to add routes or jobs repeatedly). Make sure file paths include a unique identifier to avoid collisions.
  • Share types between parents and children so settings stay in sync.

Document available subtemplates in the template repository README so teams know which features they can enable.