Writing Custom Scaffolder Templates for Node.js Services

Standardizing service creation across distributed engineering teams requires precise automation. When writing custom scaffolder templates for Node.js services, platform engineers must balance flexibility with strict governance. This builds directly on the patterns in Scaffolder Template Design; this guide details the exact configuration, validation steps, and edge-case handling required to deploy production-ready Node.js scaffolds within your internal developer portal.

Node.js skeleton rendering with conditional dependencies Parameters drive Nunjucks rendering of the skeleton, where conditional blocks inject optional dependencies before the repo is published and registered. Parameters name, owner, dbType Nunjucks render if dbType: add pg package.json valid JSON publish + register repo + catalog conditional blocks must still emit valid JSON, or the install fails
Conditional Nunjucks blocks decide which dependencies land in package.json; the rendered file must stay valid JSON before publish and registration.

Context: Standardizing Node.js Service Generation

Platform teams frequently encounter inconsistent project structures, missing CI/CD pipelines, and delayed onboarding when developers manually bootstrap repositories. By integrating a dedicated scaffolding workflow into the broader Plugin Ecosystem & Custom Extensions, organizations enforce architectural guardrails while preserving developer autonomy. The scaffolder translates high-level inputs into fully initialized Node.js projects, complete with dependency management, linting rules, and catalog metadata.

Exact Configuration: Core Template Architecture

A functional Node.js scaffold relies on three critical Backstage actions: fetch:template, publish:github (or equivalent), and catalog:register. The fetch:template action processes Nunjucks placeholders, injecting parameters like serviceName, nodeVersion, and packageManager. Ensure your template.yaml declares apiVersion: scaffolder.backstage.io/v1beta3 and maps input fields to the parameters schema. The template skeleton directory must mirror the target repository structure, with package.json containing dynamic placeholders (e.g., {{ values.serviceName }}).

Core Scaffolder Definition (template.yaml)

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: nodejs-service-template
  title: Node.js Microservice
  description: Standardized Node.js service with TypeScript and CI/CD
spec:
  owner: platform-team
  type: service
  parameters:
    - title: Service Details
      required: [name, owner]
      properties:
        name:
          title: Service Name
          type: string
          ui:field: EntityNamePicker
        owner:
          title: Owner Group
          type: string
          ui:field: EntityPicker
          ui:options:
            allowedKinds: [Group]
  steps:
    - id: fetch
      name: Fetch Template
      action: fetch:template
      input:
        url: ./skeleton
        values:
          serviceName: ${{ parameters.name }}
          owner: ${{ parameters.owner }}
    - id: publish
      name: Publish to GitHub
      action: publish:github
      input:
        allowedHosts: ['github.com']
        repoUrl: github.com?owner=${GITHUB_ORG}&repo=${{ parameters.name }}
        description: "${{ parameters.name }} service"
        defaultBranch: main
    - id: register
      name: Register Catalog
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
        catalogInfoPath: '/catalog-info.yaml'
  output:
    links:
      - title: Repository
        url: ${{ steps['publish'].output.remoteUrl }}

Dynamic Package Manifest (skeleton/package.json)

Backstage’s fetch:template uses Nunjucks templating. The skeleton files use {{ values.* }} syntax:

{
  "name": "{{ values.serviceName }}",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "node dist/index.js",
    "build": "tsc",
    "lint": "eslint . --ext .ts"
  },
  "dependencies": {
    "express": "^4.18.2"{% if values.dbType === 'postgresql' %},
    "pg": "^8.11.0"{% endif %},
    "zod": "^3.22.4"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "@types/node": "^20.10.0",
    "@types/express": "^4.17.21"
  }
}

Validation: Pre-Flight Checks & Local Testing

Execute the following validation pipeline before merging template changes to production:

  1. Schema Validation: Run npx @backstage/cli catalog validate --path ./template.yaml to catch YAML syntax errors and missing required fields.
  2. Local Simulation: Start a local Backstage instance with the scaffolder backend enabled:
    yarn dev
    
  3. Workflow Execution: Trigger the template via the local UI at http://localhost:3000/create. Inspect the temporary repository output in the configured Git provider.
  4. Dependency Resolution: Navigate to the scaffolded directory and run npm ci to verify lockfile compatibility and dependency tree integrity.
  5. Catalog Metadata Verification: Confirm catalog-info.yaml correctly resolves ${{ parameters.owner }} and ${{ parameters.name }} into valid system and owner entity references.

Edge Cases: Conditional Logic & Secret Handling

  • Conditional Dependencies: Use Nunjucks conditionals directly in package.json skeleton to inject optional drivers (e.g., {% if values.dbType === 'postgresql' %}...{% endif %}).
  • Private Registry Authentication: Never hardcode .npmrc tokens in skeleton files. Pass credentials securely via the Backstage backend’s secrets configuration or inject them through a CI/CD step post-generation.
  • Monorepo Integration: Configure fetch:template with targetPath: ./packages/{{ values.serviceName }}. Ensure the root package.json workspace configuration is updated to include the new service path post-generation using a custom action or a follow-up PR.

Common Pitfalls & Rapid Resolution

Symptom Root Cause Resolution
Template silently ignored by Backstage Missing or incorrect apiVersion Update to apiVersion: scaffolder.backstage.io/v1beta3
CI/CD pipeline fails on fetch step Hardcoded absolute paths in fetch:template Switch to relative paths (./skeleton)
Invalid JSON in generated package.json Unescaped Nunjucks delimiters in script blocks Wrap conditional blocks in valid JSON syntax or use template helpers
Service invisible in Developer Portal Skipped catalog:register step Append catalog:register action pointing to /catalog-info.yaml

Frequently Asked Questions

How do I test a custom Node.js scaffolder template locally before publishing?

Run npx @backstage/cli catalog validate against your template.yaml, then start a local Backstage instance with yarn dev. Trigger the template via the UI, inspect the generated repository, and verify catalog-info.yaml resolves all parameters correctly before committing.

Can I enforce specific Node.js versions across all scaffolded services?

Yes. Define a fixed nodeVersion parameter in the template schema, or hardcode the engines field in the skeleton package.json. You can also use fetch:template to inject .nvmrc or .node-version files automatically into the generated repository.

What happens if the catalog registration step fails after repository creation?

The repository remains intact, but the service will not appear in the portal. Implement a retry mechanism using the catalog:register action’s built-in retry behavior, or configure a webhook in the target Git provider to trigger a catalog:refresh on push events. You can also manually register the location via the Backstage API.