This installment is part 2 of a 6-part series focused on Angular Schematics.
- ✅ Part 1 — Understanding Angular Schematics: Architecture & Core Concepts
- Part 2 — Creating Custom Generators with ng generate ← You are here
- 🔜 Part 3 — Building Installation Schematics with ng add
- 🔜 Part 4 — Writing Migration Schematics with ng update
- 🔜 Part 5 — Testing Schematics with Angular DevKit
- 🔜 Part 6 — Advanced Patterns, Publishing & Nx Integration
🤖 Note about this piece: My use of Claude assisted in reformatting and structuring the text to enhance readability and presentation for this publication.
Source Materials
Preparation of this article drew from the following official Angular documentation and source references:
- Angular CLI — Schematics for Libraries — Official Angular docs explaining generation support through schematics
- Angular DevKit Schematics — Templating — The DevKit README section detailing the template engine and filename syntax
- Angular DevKit Schematics — Full README — Comprehensive reference for
@angular-devkit/schematics
Part 1 explored the internals of Angular Schematics — covering the Tree, Rule, Source, SchematicContext, and the execution pipeline. We now shift to practical implementation.
Our scope remains narrow. When you finish reading, you’ll have crafted a schematic capable of producing a single Angular component preconfigured with your team’s custom selector prefix — eliminating configuration overhead, removing the need for the --prefix flag, and ensuring uniformity throughout the codebase.
A single command:
ng generate my-org-schematics:component my-button
Produces:
CREATE src/app/my-button/my-button.component.ts
CREATE src/app/my-button/my-button.component.html
CREATE src/app/my-button/my-button.component.spec.ts
Every selector gets the acme-my-button prefix automatically, never app-my-button. That keeps things straightforward, consistent, and easy to enforce across the board.
Table of Contents
- Setting Up the Workspace
- Project Structure
- collection.json — Registering the Schematic
- schema.json — Defining Options
- The Factory Function
- The
stringsUtility — What Every Function Does - Decoding
__name@dasherize__— The Filename Template Syntax - The File Templates
- Building and Running
- Before & After
- Why This Matters — Use Cases and Enterprise Standardisation
- Summary & What’s Next
Setting Up the Workspace
Ensure the schematics CLI is installed globally, then kick off the project scaffold:
npm install -g @angular-devkit/schematics-cli
schematics blank --name=my-org-schematics
cd my-org-schematics
npm install
Move the default schematic output directory out of its initial name and into a dedicated location. Specifically, take the folder currently called
my-org-schematics/ and turn it into
component/, placing it under
src/. Before you proceed, your directory tree ought to resemble the layout shown here:
my-org-schematics/
├── package.json
├── tsconfig.json
└── src/
├── collection.json
└── component/
├── index.ts
├── index_spec.ts
└── schema.json
Project Structure
Prior to starting implementation, verify that the "schematics" entry in package.json references your collection.
{
"name": "my-org-schematics",
"version": "1.0.0",
"schematics": "./src/collection.json",
"scripts": {
"build": "tsc -p tsconfig.json"
}
}
This is the one property the Angular CLI uses to find where your collection of schematics lives. If this field is missing, ng generate has no way to tell that your package is there.
collection.json — Registering the Schematic
Go into src/collection.json and swap out what it currently holds:
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"component": {
"description": "Generates a component with the organisation selector prefix",
"factory": "./component/index#component",
"schema": "./component/schema.json"
}
}
}
The factory entry ./component/index#component instructs the runner: load ./component/index.js (the compiled artifact) and invoke its exported component function. The # symbol acts as a delimiter between the module path and the target export.
schema.json — Defining Options
The schema enumerates all possible options for the schematic. It governs validation, presets defaults, and triggers command-line prompts whenever a flag is omitted.
Swap in the content for src/component/schema.json:
{
"$schema": "http://json-schema.org/schema",
"$id": "ComponentSchema",
"title": "Organisation Component Schematic",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "The name of the component",
"$default": { "$source": "argv", "index": 0 },
"x-prompt": "What should the component be called?"
},
"prefix": {
"type": "string",
"description": "The selector prefix to use",
"default": "acme"
},
"path": {
"type": "string",
"description": "Where to create the component",
"default": "src/app"
}
},
"required": ["name"]
}
There are three important details here:
"$default": {"$source": "argv", "index": 0}associates the initial positional argument withname, which enablesng generate my-org-schematics:component my-buttonwithout requiring the-nameflag.- When
nameis missing, the"x-prompt"property triggers an interactive CLI prompt. prefixhas a default value of"acme"— a single, hard-coded organisation prefix that never needs to be re-entered.
Now, create the corresponding TypeScript interface in src/component/schema.ts:
export interface ComponentSchema {
name: string;
prefix: string;
path: string;
}
The Factory Function
At the heart of the schematic lies this factory function. It takes the options that were validated earlier, constructs a template source using the contents of the files/ folder, and applies that source to the Tree representing the workspace.
Substitute the current content of src/component/index.ts with the following:
import {
Rule,
SchematicContext,
Tree,
apply,
applyTemplates,
mergeWith,
move,
url,
} from '@angular-devkit/schematics';
import { strings, normalize } from '@angular-devkit/core';
import { ComponentSchema } from './schema';
export function component(options: ComponentSchema): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.info(`Generating component: ${options.name}`);
// Where the files will land
const targetPath = normalize(
`${options.path}/${strings.dasherize(options.name)}`
);
const templateSource = apply(url('./files'), [
applyTemplates({
// String utility functions available inside templates
...strings,
// Options available inside templates
name: options.name,
prefix: options.prefix,
}),
move(targetPath),
]);
return mergeWith(templateSource);
};
}
That is the complete factory — just 30 lines. Here’s a breakdown of its three critical operations:
url('./files')— designates the template folder we will set up.applyTemplates({...})— runs every template file through name and content transformation, filling in the variable values.move(targetPath)— delivers the generated output to the workspace at the proper location.
The strings Utility — Quick Reference
By inserting ...strings into applyTemplates() with the spread operator, all utility functions become accessible within your template files and their filenames. This comes from @angular-devkit/core:
import { strings } from '@angular-devkit/core';
| Function | Output | Example | Use case |
|---|---|---|---|
dasherize |
kebab-case |
UserCard → user-card |
File names, selectors, import paths |
classify |
PascalCase |
user-card → UserCard |
Class names, module names |
camelize |
camelCase |
user-card → userCard |
Variable names, constructor params |
underscore |
snake_case |
UserCard → user_card |
Config keys, backend interop |
capitalize |
First letter up | userCard → UserCard |
Titles, first character only |
decamelize |
space separated |
UserCard → user card |
Human-readable labels |
Every one of the six functions is idempotent, meaning that when you feed them already-formatted input, they hand back the exact same value. In the course of building most schematics, dasherize and classify become your go-to utilities. The remaining ones are reserved for more niche scenarios.
Demystifying __name@dasherize__ — Understanding Template File Naming
Because file paths forbid the use of <, >, and %, template filenames rely on a distinct syntax. Any text enclosed between double underscores __ is interpreted as a template expression that operates on the filename itself — it doesn’t touch the file’s contents.
__name@dasherize__.component.ts.template
↑ ↑
variable transform function (optional)
The
@ acts as the pipe operator. Any function provided to
applyTemplates() can serve as the transformation. When you execute
ng generate my-org-schematics:component UserProfileCard, the lookup proceeds in the following manner:
__name@dasherize__.component.ts.template
↓ dasherize("UserProfileCard") → "user-profile-card"
user-profile-card.component.ts ← final filename (`.template` stripped automatically)
The same pattern is also applicable to directory names — for instance, __name@dasherize__/ is rendered as user-profile-card/ in the result.
When dealing with file content, the corresponding syntax takes the form of <%= dasherize(name) %>. Both approaches represent the identical idea but are used in distinct scenarios:
// filename: __name@dasherize__.component.ts.template
// file content: selector: '<%= prefix %>-<%= dasherize(name) %>'
When a filename requires a multi-step transformation that a lone function can’t cover, perform the calculation inside
applyTemplates() and expose it under an explicit key:
applyTemplates({
...strings,
name: options.name,
prefixedName: `${options.prefix}-${strings.dasherize(options.name)}`,
})
// then use __prefixedName__ in the filename
The File Templates
Within src/component/, add a folder named files/. The names of template files follow the __variableName@transformFunction__ pattern, which the DevKit processes during file creation.
src/component/files/
├── __name@dasherize__.component.ts.template
├── __name@dasherize__.component.html.template
└── __name@dasherize__.component.spec.ts.template
Component class
// __name@dasherize__.component.ts.template
import { Component } from '@angular/core';
@Component({
selector: '<%= prefix %>-<%= dasherize(name) %>',
templateUrl: './<%= dasherize(name) %>.component.html',
})
export class <%= classify(name) %>Component {}
Component template
<!-- __name@dasherize__.component.html.template -->
<p><%= dasherize(name) %> works!</p>
Component spec
// __name@dasherize__.component.spec.ts.template
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';
describe('<%= classify(name) %>Component', () => {
let fixture: ComponentFixture<<%= classify(name) %>Component>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [<%= classify(name) %>Component],
}).compileComponents();
fixture = TestBed.createComponent(<%= classify(name) %>Component);
});
it('should create', () => {
expect(fixture.componentInstance).toBeTruthy();
});
});
The template processor relies on <%= expression %> to insert a value directly into the output. Any key supplied to applyTemplates() — such as name, prefix, or one of the strings helpers like classify or dasherize — becomes accessible in all template files.
Build and Execute
Start by compiling your TypeScript sources into JavaScript, then create a link to the resulting package within an Angular workspace so you can test it.
# In your schematics project
npm run build
npm link
# In your Angular workspace
npm link my-org-schematics
# Run it
ng generate my-org-schematics:component my-button
# Preview without touching disk
ng generate my-org-schematics:component my-button --dry-run
# Override the prefix for a specific run
ng generate my-org-schematics:component my-button --prefix=ui
Before & After
When you run ng generate my-org-schematics:component my-button and stick with the default acme prefix, you end up with these three files.
src/app/my-button/my-button.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'acme-my-button',
templateUrl: './my-button.component.html',
})
export class MyButtonComponent {}
src/app/my-button/my-button.component.html
<p>my-button works!</p>
src/app/my-button/my-button.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MyButtonComponent } from './my-button.component';
describe('MyButtonComponent', () => {
let fixture: ComponentFixture<MyButtonComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MyButtonComponent],
}).compileComponents();
fixture = TestBed.createComponent(MyButtonComponent);
});
it('should create', () => {
expect(fixture.componentInstance).toBeTruthy();
});
});
See how the selector stacks up against the output of Angular’s standard ng generate component:
| Selector | |
|---|---|
Built-in ng generate component |
app-my-button |
| Our custom schematic | acme-my-button |
Every component your team generates from this point forward will carry the correct prefix — no one needs to recall the flag or consult a style guide.
Why This Matters — Use Cases and Enterprise Standardisation
The selector prefix example we built is deliberately simple. However, the pattern translates directly into the everyday challenges faced by large Angular teams.
At its heart, the principle is this: architectural rules either reside in documentation that people rarely follow consistently, or they live in a schematic that enforces them automatically. Schematics convert a style guide into infrastructure.
Common use cases
Uniform scaffolding across large teams. The selector prefix issue is just one manifestation of a broader problem — developers making different choices when generating code. A shared schematic eliminates the decision entirely. Selectors, folder layout, barrel files, import paths — all correct, every time, no matter who executes the command.
Domain-specific code patterns. The standard ng generate component knows nothing about your application. If your team consistently pairs a component with a facade service, or generates NgRx feature scaffolding as a complete unit (actions + reducer + selectors + effects), a custom schematic can produce the whole pattern in a single command instead of four.
Quicker onboarding. A new developer who runs ng generate acme:component on their first day gets the same result as a senior engineer with two years of experience on the project. They absorb the pattern by doing, not by reading. A thoughtfully named collection — ng generate acme:component, acme:feature, acme:api-service — documents itself through --help.
Preventing drift in monorepos. In a multi-team Angular monorepo, each team must produce code that adheres to organisation-wide standards. A shared schematics package published to an internal npm registry (Verdaccio, Artifactory, GitHub Packages) acts as the definitive reference. Architectural changes are rolled out as a new package version and deployed via ng update — a single update spreads the standard to every team.
A typical internal schematics collection might resemble this:
ng generate acme-platform:component → component with design system wiring
ng generate acme-platform:feature → full NgRx feature scaffold
ng generate acme-platform:api-service → HTTP service with org interceptor wiring
ng generate acme-platform:form-page → reactive form with standard validation pattern
ng generate acme-platform:data-table → container + table + service, pre-connected
Every entry represents a one-time architectural choice that has now been permanently encoded. Folder structure debates no longer appear in code reviews because that decision was finalized when the schematic was authored.
Handling breaking and non-breaking changes in a shared component library
At enterprise scale, schematics show their true power here — and the return on investment becomes substantial.
Imagine a major platform maintaining an in-house component library: @acme/ui. The library includes 40+ business components — grids, controls, dialogs, navigation shells — and serves 30 Angular applications alongside 15 internal libraries. The @acme/ui team iterates rapidly. APIs evolve continuously. Components get renamed. Input types shift. Long-deprecated patterns finally disappear.
Absent schematics, each breaking change forces a migration document that 30 separate teams must read, interpret, and apply manually — at staggered times, with varying diligence, yielding inconsistent outcomes.
With schematics, the change arrives as executable code — and the CLI applies it without human intervention.
When @acme/ui releases a fresh version, consumers execute ng update @acme/ui. The CLI detects the version difference, triggers the relevant migration schematics, and writes all modifications to disk. Teams review the diff and commit. Here’s how this operates across different change categories:
Non-breaking change — an additional required input with a harmless default. @acme/ui introduces a variant input to AcmeButtonComponent, defaulting to 'primary'. The migration schematic searches every <acme-button> usage in template files workspace-wide and explicitly inserts variant="primary" — surfacing the implicit default and preparing the codebase for eventual removal of that default.
Breaking change — component selector renamed. During a design system consolidation, acme-data-grid becomes acme-table. The migration schematic scans all .html template files for <acme-data-grid and </acme-data-grid>, replaces the elements, and adjusts corresponding module import paths. Every consumer workspace sees consistent updates delivered in under a minute.
The core schematic logic — pulling files from the Tree, executing transformations, persisting results — matches precisely what we’ve constructed throughout this article. What varies is the delivery method: these schematics appear in migrations.json and execute automatically through ng update, rather than being called manually via ng generate.
🔔 What’s next: Deep coverage of
ng updatemigrations — includingmigrations.json, version-gating, and TypeScript AST transformations — arrives in Part 4 of this series.
Repository-wide operational changes through schematics
Schematics extend beyond code modifications to drive operational and infrastructure transitions spanning entire ecosystems of Angular repositories. This scenario remains underutilized — yet it perfectly illustrates why chain was created.
Take a realistic scenario: your company switches from Bitbucket to GitHub, DevOps introduces a standardized CI pipeline, security now mandates SECURITY.md and CODEOWNERS files across every repository, and the platform transitions to another secrets management provider. Individually, these represent 30 separate tickets. Combined, they’re a single schematic.
The platform team ships @acme/platform-tools and each team invokes one command:
bash
ng generate @acme/platform-tools:repo-standardise
Inside, the factory chains every operation as an independent Rule:
typescript
export function repoStandardise(options: Schema): Rule { return chain([ createGitHubWorkflow(), // create .github/workflows/ci.yml from template removeOldPipelineFile(), // delete bitbucket-pipelines.yml if present updateCiNodeVersion(), // patch Node.js version in the new ci.yml enforceComplianceFiles(), // create SECURITY.md + CODEOWNERS if missing migrateSecretsProvider(), // update environment.ts import + initialisation ]); }
A single command. A single diff to check. Every repo on the platform ends up in a verified state. What used to mean 30 individual tickets and a 6-week process turns into a team running one ng generate that takes half a minute.
🔔 Stay tuned: For schematics that have to install npm dependencies or handle first-time package configuration,
ng addis the proper path — detailed in Part 3. When modifications must run automatically upon a team's package version upgrade,ng updatehandles it — detailed in Part 4.
ng generate vs ng update — Picking the correct delivery method
Once teams get serious about authoring schematics, a recurring question emerges: does this belong in ng generate or ng update? The underlying schematic operations — Tree mutations, file edits, overwrites — tend to look the same. What really separates them is the trigger time, the trigger mechanism, and who has authority over the process.
Grasping that difference avoids the error we fixed earlier: turning to ng generate for a version migration that should be handled by ng update.
ng generate |
ng update |
|
|---|---|---|
| Trigger | Manual — developer runs the command explicitly | Automatic — CLI runs it as part of a package version upgrade |
| Version awareness | None — no knowledge of current or target version | Full — runs only migrations applicable to the version delta |
| Registered in | collection.json |
migrations.json |
| Idempotency | Developer’s responsibility to guard | CLI guarantees — each migration runs exactly once per version |
| Consumer control | Explicit opt-in — developer decides when to run | Implicit — fires automatically on ng update |
| Best for | On-demand scaffolding, one-off operational tasks, opt-in changes | Library API migrations tied to a version bump |
Keeping it maintainable
Maintaining a shared schematics package is easier if you follow these proven habits:
- Keep it versioned alongside your component library. When the library evolves, the schematic and a migration (Part 4) are released together.
- Test every schematic. Flawed generated code at scale is worse than having no schematic at all. Part 5 of this series explores
SchematicTestRunnerandUnitTestTreein detail. - Write the
descriptionfields inschema.jsonas if they are the docs — for most developers on your team,ng generate acme: --helpis the only documentation they will ever consult.
Summary & What’s Next
The example above was reduced to its simplest viable form on purpose. It consists of four files, a single dependency, and adheres to a one-concepts-per-section approach. Here is what was accomplished:
- A schematics workspace linked to the Angular CLI through the
package.json - A
collection.jsonfile that registers the schematic under its chosen name - A
schema.jsonthat specifies options, default values, and interactive prompts - A factory function that places templates into the designated target directory
- Three template files that together yield a whole, internally consistent component
Moving forward, expansion is quite direct. If automatic addition of the component to an existing module is desired, merely attach another Rule to the chain. The setup is robust enough to build upon.
Part 3 will change focus from generating code to incorporating libraries. That installment will construct an ng add schematic—the kind activated when a user executes ng add my-ui-library for the first time—tackling dependency setup, modifications to angular.json, and wiring it into a module without manual intervention.
Series Roadmap
| Part | Topic | Status |
|---|---|---|
| Part 1 | Understanding Angular Schematics — Architecture & Core Concepts | ✅ Published |
| Part 2 | Creating Custom Generators with ng generate | ✅ You are here |
| Part 3 | Building Installation Schematics with ng add | 🔜 Coming Soon |
| Part 4 | Writing Migration Schematics with ng update | 🔜 Coming Soon |
| Part 5 | Testing Schematics with Angular DevKit | 🔜 Coming Soon |
| Part 6 — Advanced Patterns, Publishing & Nx Integration | 🔜 Coming Soon |
Compiled using Angular v21 · @angular-devkit/schematics · @angular-devkit/schematics-cli · @angular-devkit/core


