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 aTemplateConfigModule
. Define metadata (name
,description
,specVersion
), the Zod schemas for user input (templateSettingsSchema
andtemplateFinalSettingsSchema
), and themapFinalSettings
function that produces the values used during rendering. SettargetPath
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 totargetPath
.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.