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 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 EJS or Nunjucks placeholders, injecting parameters like service_name, node_version, and package_manager. Ensure your template.yaml declares apiVersion: scaffolder.backstage.io/v1beta3 and maps input fields to the parameters schema. The template directory must mirror the target repository structure, with package.json and tsconfig.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
steps:
- id: fetch
name: Fetch Template
action: fetch:template
input:
url: ./skeleton
values:
serviceName: ${{ parameters.name }}
- id: publish
name: Publish to GitHub
action: publish:github
input:
allowedHosts: ['github.com']
description: ${{ parameters.name }} service
- id: register
name: Register Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: '/catalog-info.yaml'
Dynamic Package Manifest (skeleton/package.json)
{
"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\",<% } %>"
"zod": "^3.22.4"
},
"devDependencies": {
"typescript": "^5.3.0",
"@types/node": "^20.10.0"
}
}
Validation: Pre-Flight Checks & Local Testing
Execute the following validation pipeline before merging template changes to production:
- Schema Validation: Run
yarn backstage-cli templates:validate --path ./path/to/template.yamlto catch YAML syntax errors and missing required fields. - Local Simulation: Start an isolated scaffolder backend instance:
yarn start --filter @backstage/plugin-scaffolder-backend
- Workflow Execution: Trigger the template via the local UI. Inspect the temporary repository output.
- Dependency Resolution: Navigate to the scaffolded directory and run
npm cioryarn install --frozen-lockfileto 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 EJS conditionals directly in
package.jsonanddocker-compose.ymlto inject optional drivers (e.g.,<% if (values.dbType === 'postgresql') { %>...<% } %>). - Private Registry Authentication: Never hardcode
.npmrctokens. Pass credentials securely via thepublishaction’stokenparameter or inject them through Backstage’s secret management API at runtime. - Monorepo Integration: Configure
fetch:templatewithtargetPath: ./packages/${values.serviceName}. Ensure the rootpackage.jsonworkspace configuration is updated to include the new service path post-generation.
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 EJS 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 yarn backstage-cli templates:validate against your template.yaml, then start a local Backstage instance with the scaffolder plugin enabled. Trigger the template via the UI and inspect the generated repository in a temporary GitHub/GitLab organization.
Can I enforce specific Node.js versions across all scaffolded services?
Yes. Define a fixed node_version parameter in the template schema, or hardcode the engines field in the skeleton package.json. You can also use a fetch:template post-action to inject .nvmrc or .node-version files automatically.
What happens if the catalog registration step fails after repository creation?
The repository remains intact, but the service won’t appear in the portal. Implement a retry mechanism in the catalog:register step or configure a webhook in the target Git provider to trigger a manual catalog:refresh on push events.