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

  • @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

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]

Validation

# 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"

Edge Cases & Troubleshooting

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.