Build your first template
This tutorial walks you through building a template end-to-end. By the end you will have a published Go CLI template that others can scaffold with a single command.
1. Create the scaffold
spin init my-go-cli
This writes a skeleton:
my-go-cli/
spin.toml
_base/
file.txt.tmpl
README.md
Delete _base/file.txt.tmpl — we will write real files.
2. Add params
Open spin.toml and replace its contents:
name = "my-go-cli"
description = "A minimal Go CLI starter"
language = "go"
type = "cli"
version = "0.1.0"
[params]
project_name = { type = "text", prompt = "Project name", default = "myapp" }
module = { type = "text", prompt = "Go module path", default = "github.com/me/{{ .project_name }}" }
port = { type = "number", prompt = "Port", default = 8080, min = 1024, max = 65535 }
license = { type = "license", prompt = "What license do you want?", default = "MIT" }
module uses a templated default: it fills in {{ .project_name }} from the value given to spin new. port enforces a range.
3. Write template files
Create _base/main.go.tmpl:
package main
import "fmt"
func main() {
fmt.Println("{{ .project_name }} listening on :{{ .port }}")
}
Create _base/go.mod.tmpl:
module {{ .module }}
go 1.22
Create _base/LICENSE.tmpl:
{{- if eq .license "MIT" }}
MIT License
Copyright (c) {{ now "2006" }} {{ .project_name }}
Permission is hereby granted, free of charge...
{{- else if eq .license "Apache-2.0" }}
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
{{- else }}
All rights reserved.
{{- end }}
Notice the whitespace control: {{- eats the leading blank line so the output starts at the first character.
Create _base/README.md.tmpl:
# {{ .project_name | title }}
A Go CLI project.
## Usage
```sh
go run .
```
- License: {{ .license }}
4. Add hooks
Back in spin.toml, add post-hooks to initialise the Go module and git:
[[post]]
run = "go mod init {{ .module }}"
[[post]]
run = "go mod tidy"
[[post]]
run = "git init && git add -A && git commit -m 'initial commit'"
Hook commands only support field access ({{ .module }}), not pipes or custom functions. If you need a transformed value, use shell tools instead.
5. Test it
spin new test-output --template ./my-go-cli --dry-run
This renders to a temp directory and lists the files. No hooks run. Check the output looks right, then test with params:
spin new test-output --template ./my-go-cli --param license=Apache-2.0 --print-params
Try the full interactive flow:
spin new test-output --template ./my-go-cli
Answer the params form, review the hooks, press R then Run. The project scaffolds into ./test-output. Delete test-output between runs.
6. Publish
Push your template to a git repo:
cd my-go-cli
git init && git add -A && git commit -m "initial template"
git remote add origin https://github.com/YOU/my-go-cli.git
git push -u origin main
Now anyone can scaffold from it:
spin new myapp --template https://github.com/YOU/my-go-cli.git
To make it discoverable, publish it in a registry.
What you learned
spin.tomldeclares params, hooks, and includes._base/*.tmplfiles use Go'stext/templatewith spin's helpers.{{ .param }}for field access,{{ if }}for conditions,{{ range }}for loops.{{-trims leading whitespace.- Hooks are bare field access only — use shell for transformations.
--dry-runand--print-paramspreview before you commit.- Push to a git repo to publish.