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.
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:
- Schema Validation: Run
npx @backstage/cli catalog validate --path ./template.yamlto catch YAML syntax errors and missing required fields. - Local Simulation: Start a local Backstage instance with the scaffolder backend enabled:
yarn dev - Workflow Execution: Trigger the template via the local UI at
http://localhost:3000/create. Inspect the temporary repository output in the configured Git provider. - Dependency Resolution: Navigate to the scaffolded directory and run
npm cito verify lockfile compatibility and dependency tree integrity. - Catalog Metadata Verification: Confirm
catalog-info.yamlcorrectly resolves${{ parameters.owner }}and${{ parameters.name }}into validsystemandownerentity references.
Edge Cases: Conditional Logic & Secret Handling
- Conditional Dependencies: Use Nunjucks conditionals directly in
package.jsonskeleton to inject optional drivers (e.g.,{% if values.dbType === 'postgresql' %}...{% endif %}). - Private Registry Authentication: Never hardcode
.npmrctokens 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:templatewithtargetPath: ./packages/{{ values.serviceName }}. Ensure the rootpackage.jsonworkspace 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.
Related
- Scaffolder Template Design — the parent guide on parameterization, action sequencing, and versioning
- Plugin Ecosystem & Custom Extensions — the section on extension architecture and governance
- Catalog Integration Patterns — how the generated
catalog-info.yamlis ingested and resolved