Creating a Golden Path Template for Microservices

This how-to walks through authoring a complete, version-pinned Backstage software template that scaffolds a production-ready HTTP microservice: source skeleton, CI workflow, container build, and catalog registration. It is the concrete implementation behind the Golden Paths & Paved Roads sub-section of Developer Experience & Self-Service Platforms, and it reuses the action vocabulary documented in Scaffolder Template Design.

Prerequisites

Authoring a golden-path template needs four things: the scaffolder with its GitHub module, the CLI for dry-runs, a scoped ${GITHUB_TOKEN}, and a template repository referenced from catalog.locations by an explicit tag.

Prerequisites for authoring a golden-path template The scaffolder with GitHub module, the CLI, a scoped token, and a tagged template repo make the template authorable. scaffolder + github CLI for dry-run scoped token tagged template repo Authorable versioned path
Referencing the template by an explicit ?ref= tag is what lets a bad revision be validated before developers see it.
  • @backstage/plugin-scaffolder-backend@^1.22.0 and @backstage/plugin-scaffolder-backend-module-github@^0.5.0 installed and registered in the backend.
  • @backstage/cli@^0.27.0 available for validation and dry-runs.
  • A GitHub organization and a token in ${GITHUB_TOKEN} with repo and workflow scopes, injected via app-config.yaml.
  • A ${CONTAINER_REGISTRY} host value and at least one Group entity in the catalog to serve as an owner.
  • A Git repository to hold templates (for example golden-paths), referenced from catalog.locations with an explicit ?ref= tag.

Exact Configuration

The template has two parts that map cleanly to the three actions: a Template definition drives the flow, and a skeleton/ directory holds the files that fetch:template renders, publish:github commits, and catalog:register indexes.

Template definition and skeleton mapped to actions The skeleton directory is rendered by fetch:template, committed by publish:github, and indexed by catalog:register. skeleton/ Nunjucks files fetch:template render values publish:github commit repo register index entity
Files must live under the url directory passed to fetch:template, or their placeholders ship unrendered.

1. Lay out the template directory

golden-paths/templates/microservice/
  microservice.yaml          # the Template definition
  skeleton/                  # files rendered into the new repo
    catalog-info.yaml
    Dockerfile
    .github/workflows/ci.yaml
    cmd/server/main.go

2. Author the Template definition

# templates/microservice/microservice.yaml
# Requires scaffolder.backstage.io/v1beta3 (Backstage >= 1.22.0)
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: golden-path-microservice
  title: Golden Path — HTTP Microservice
  description: Provisions a Go HTTP service with CI, container build, and catalog entry
  tags: [golden-path, microservice]
spec:
  owner: group:platform-engineering
  type: service
  parameters:
    - title: Service identity
      required: [name, owner]
      properties:
        name:
          title: Service name
          type: string
          pattern: "^[a-z][a-z0-9-]{2,38}$"
          ui:autofocus: true
        owner:
          title: Owning team
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter: { kind: Group }
  steps:
    - id: render
      name: Render skeleton
      action: fetch:template
      input:
        url: ./skeleton
        values:
          name: ${{ parameters.name }}
          owner: ${{ parameters.owner }}
          registry: ${CONTAINER_REGISTRY}

    - id: publish
      name: Create repository
      action: publish:github
      input:
        repoUrl: github.com?owner=${GITHUB_ORG}&repo=${{ parameters.name }}
        defaultBranch: main
        protectDefaultBranch: true
        topics: [golden-path, microservice]

    - id: register
      name: Register component
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

  output:
    links:
      - title: Open repository
        url: ${{ steps.publish.output.remoteUrl }}
      - title: View in catalog
        icon: catalog
        entityRef: ${{ steps.register.output.entityRef }}

3. Author the skeleton’s catalog entry

Templated fields use the {{ }} Nunjucks syntax so they resolve at render time.

# skeleton/catalog-info.yaml
# Requires backstage.io/v1alpha1
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: ${{ values.name }}
  annotations:
    backstage.io/techdocs-ref: dir:.
spec:
  type: service
  lifecycle: experimental
  owner: ${{ values.owner }}

4. Provide opinionated CI

# skeleton/.github/workflows/ci.yaml
# Requires GitHub Actions; image pushed to ${CONTAINER_REGISTRY}
name: ci
on: { push: { branches: [main] }, pull_request: {} }
jobs:
  build:
    runs-on: ubuntu-latest
    permissions: { contents: read, id-token: write }
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with: { go-version: "1.22" }
      - run: go test ./...
      - name: Build image
        run: docker build -t ${{ '${{' }} secrets.REGISTRY {{ '}}' }}/${{ values.name }}:${{ '${{' }} github.sha {{ '}}' }} .

5. Register the template location

# app-config.yaml
# Requires Backstage >= 1.22.0
catalog:
  locations:
    - type: url
      target: https://github.com/${GITHUB_ORG}/golden-paths/blob/v2.4.0/templates/microservice/microservice.yaml
      rules:
        - allow: [Template]

Validate in four passes: schema-validate the template, lint the skeleton YAML, confirm a dry-run leaves no unresolved placeholder, and verify the generated component resolves a group owner.

Four golden-path template validation passes Schema validation, skeleton lint, a placeholder scan, and an owner-resolution check validate the template. Schema validate 0 errors Lint skeleton yamllint No placeholders clean render Owner resolves group: prefix
A surviving unrendered placeholder in the dry-run output is the surest sign a skeleton file rendered outside fetch:template.
# Requires @backstage/cli >= 0.27.0
set -euo pipefail

# 1. Schema-validate the template
npx @backstage/cli catalog validate --path ./templates/microservice/microservice.yaml
# expected: "Validated 1 entity ... 0 errors"

# 2. Lint skeleton YAML
yamllint ./templates/microservice/skeleton
# expected: no errors

# 3. Confirm no unresolved placeholders survive a dry-run render
grep -R '\${{' ./dry-run-output && echo "FAIL" || echo "clean render"
# expected: "clean render"

# 4. Verify the generated component resolves an owner
yq '.spec.owner' ./dry-run-output/catalog-info.yaml | grep -qE '^group:' && echo "owner ok"
# expected: "owner ok"

The template pitfalls split into rendering issues (literal placeholders, mangled Actions expressions) and provider issues (403s, missing owner, stale cache). The diagram sorts them so the fix targets the skeleton or the provider config.

Golden-path template pitfalls by class Literal placeholders and mangled Actions expressions are rendering issues; 403s, missing owner, and stale cache are provider issues. Rendering literal placeholder mangled Actions → fetch dir + escape Provider publish 403 no owner / cache → scope token + bump ref
Escaping only CI-workflow expressions keeps Nunjucks and GitHub Actions from fighting over the same expression syntax.
Symptom Root Cause Resolution
${{ values.name }} appears literally in the created repo Skeleton file not processed by fetch:template Ensure the file lives under the url directory passed to fetch:template, not copied via fetch:plain
publish:github fails with 403 ${GITHUB_TOKEN} lacks repo/workflow scope or is not org-authorized Reissue a fine-grained token with both scopes and authorize it for the org
Catalog shows the component with no owner OwnerPicker returned a name, not an entity ref Reference ${{ parameters.owner }} directly; the picker already yields group:default/...
GitHub Actions expression mangled by the template Nunjucks consumes ${{ }} meant for Actions Escape Actions expressions with ${{ '${{' }} ... {{ '}}' }} as shown in the CI skeleton
New template revision not picked up Backend cached the old ?ref= Bump the tag in catalog.locations and restart the backend

Frequently Asked Questions

Where should the templated CI expressions and the scaffolder expressions be disambiguated?

Both Backstage’s Nunjucks and GitHub Actions use ${{ }}. Inside a skeleton file the scaffolder evaluates first, so any expression meant for Actions must be escaped (${{ '${{' }} ... {{ '}}' }}). Keep the escaping confined to CI workflow files; everything else in the skeleton is yours to template freely.

How do I add a second language without forking the template?

Add a language enum parameter and select the skeleton directory with fetch:template pointing at ./skeleton/{{ parameters.language }}. This keeps one maintained golden path with per-language sub-skeletons rather than duplicating the entire template.