Creating Custom Generators with ng generate
This is Part 2 of a 6-part series 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
🤖 A note on this article: I used Claude to help reformat and structure the content to make it clearer and more presentable for publication.
References
The following official Angular documentation and source references were used in preparing this article:
- Angular CLI — Schematics for Libraries — Official Angular docs on providing generation support via schematics
- Angular DevKit Schematics — Templating — The DevKit README section covering the template engine and filename syntax
- Angular DevKit Schematics — Full README — Complete reference for
@angular-devkit/schematics
In Part 1 we covered the architecture of Angular Schematics — the Tree, Rule, Source, SchematicContext, and the execution pipeline. Now it’s time to build something real.
We are going to keep this focused. By the end of this article you will have written a schematic that generates a single Angular component with your team’s custom selector prefix baked in — no configuration, no remembering the --prefix flag, no inconsistency across the codebase.
One 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
Where every selector is automatically prefixed — acme-my-button instead of app-my-button. Simple, repeatable, enforceable.
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
Install the schematics CLI globally if you haven’t already, then scaffold the project:
npm install -g @angular-devkit/schematics-cli
schematics blank --name=my-org-schematics
cd my-org-schematics
npm install
Rename the default generated schematic folder from
my-org-schematics/ to
component/ inside
src/. Your starting structure should look like this:
my-org-schematics/
├── package.json
├── tsconfig.json
└── src/
├── collection.json
└── component/
├── index.ts
├── index_spec.ts
└── schema.json
Project Structure
Before writing any code, confirm that package.json has the "schematics" field pointing at your collection:
{
"name": "my-org-schematics",
"version": "1.0.0",
"schematics": "./src/collection.json",
"scripts": {
"build": "tsc -p tsconfig.json"
}
}
This is the single field the Angular CLI reads to locate your schematic collection. Without it, ng generate will not know your package exists.
collection.json — Registering the Schematic
Open src/collection.json and replace its contents:
{
"$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 value ./component/index#component means: look in ./component/index.js (the compiled output) and call the exported function named component. The # separates the file path from the export name.
schema.json — Defining Options
The schema describes every option the schematic accepts. It drives validation, default values, and — when you skip a flag — interactive prompts in the terminal.
Replace 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"]
}
Three things to note:
"$default": {"$source": "argv", "index": 0}maps the first positional argument toname, song generate my-org-schematics:component my-buttonworks without typing-name."x-prompt"makes the CLI ask interactively ifnameis not provided.prefixdefaults to"acme"— your organisation prefix, hard-coded once, never forgotten.
Add the matching TypeScript interface at src/component/schema.ts:
export interface ComponentSchema {
name: string;
prefix: string;
path: string;
}
The Factory Function
This is the core of the schematic. It receives the validated options, builds a template source from the files/ directory, and merges it into the workspace Tree.
Replace src/component/index.ts:
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’s the entire factory — 30 lines. Let’s walk through the three key calls:
url('./files')— points to the template directory we’re about to create.applyTemplates({...})— processes every template file’s name and content, substituting variables.move(targetPath)— places the generated files at the correct path in the workspace.
The strings Utility — Quick Reference
When you spread ...strings into applyTemplates(), every function in the utility becomes available inside your template files and template filenames. Imported 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 |
All six functions are idempotent — passing already-formatted input returns it unchanged. You will reach for dasherize and classify in nearly every schematic you write. The others come into play for specific patterns.
Decoding __name@dasherize__ — The Filename Template Syntax
Template filenames use a special syntax because file paths can’t contain <, >, or % characters. Everything between double underscores __ is a template expression applied to the filename itself — not the file content.
__name@dasherize__.component.ts.template
↑ ↑
variable transform function (optional)
The
@ is the pipe operator. You can use any function passed into
applyTemplates() as the transform. Running
ng generate my-org-schematics:component UserProfileCard resolves like this:
__name@dasherize__.component.ts.template
↓ dasherize("UserProfileCard") → "user-profile-card"
user-profile-card.component.ts ← final filename (`.template` stripped automatically)
The same syntax works on directory names too — __name@dasherize__/ becomes user-profile-card/ in the output.
Inside file content, the equivalent syntax is <%= dasherize(name) %>. The two are the same concept in different contexts:
// filename: __name@dasherize__.component.ts.template
// file content: selector: '<%= prefix %>-<%= dasherize(name) %>'
If you need a complex transformation on a filename that can’t be expressed with a single function, compute it in
applyTemplates() and give it an explicit name:
applyTemplates({
...strings,
name: options.name,
prefixedName: `${options.prefix}-${strings.dasherize(options.name)}`,
})
// then use __prefixedName__ in the filename
The File Templates
Create a files/ directory inside src/component/. Template filenames use __variableName@transformFunction__ syntax — the DevKit resolves these before writing.
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 engine uses <%= expression %> to output a value inline. Every key you passed into applyTemplates() — name, prefix, and all the strings utilities like classify and dasherize — is available inside every template file.
Building and Running
Build the TypeScript to JavaScript, then link it into an Angular workspace for testing:
# 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
Running ng generate my-org-schematics:component my-button with the default acme prefix produces 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();
});
});
Compare the selector to what Angular’s built-in ng generate component would produce:
| 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 use the correct prefix — without anyone having to remember the flag or read a style guide.
Why This Matters — Use Cases and Enterprise Standardisation
The selector prefix example we built is small by design. But the pattern scales directly into the problems large Angular teams deal with every day.
The core idea is simple: architectural conventions either live in documentation nobody consistently follows, or in a schematic that enforces them automatically. Schematics turn a style guide into infrastructure.
Common use cases
Consistent scaffolding across a large team. The selector prefix problem is a symptom of a wider issue — different developers making slightly different choices when generating code. A shared schematic removes the decision entirely. Selectors, folder structure, barrel files, import paths — all correct, every time, regardless of who runs the command.
Domain-specific code patterns. The built-in ng generate component knows nothing about your application. If your team always pairs a component with a facade service, or always creates NgRx feature scaffolding as a unit (actions + reducer + selectors + effects), a custom schematic can generate the whole pattern in one command rather than four.
Faster onboarding. A new developer who runs ng generate acme:component on day one gets the same output as a senior developer who has been on the project for two years. They learn the pattern by using it, not by reading about it. A well-named collection — ng generate acme:component, acme:feature, acme:api-service — is itself self-documenting via --help.
Preventing drift in monorepos. In a multi-team Angular monorepo, every team writes code that needs to conform to organisation-wide standards. A shared schematics package published to an internal npm registry (Verdaccio, Artifactory, GitHub Packages) becomes the single source of truth. Architecture changes are published as a new package version and distributed via ng update — one update propagates the standard to every team.
A mature internal schematics collection might look like:
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
Each entry is an architectural decision that was made once and encoded permanently. Nobody debates folder structure in a code review because the folder structure was settled when the schematic was written.
Managing breaking and non-breaking changes in a shared component library
This is where schematics become genuinely powerful at enterprise scale — and where the investment pays back many times over.
Picture a large platform with a home-grown component library: @acme/ui. It ships 40+ business components — data grids, form controls, dialogs, navigation shells — and is consumed by 30 Angular applications and a further 15 internal libraries. The team that owns @acme/ui moves fast. APIs evolve. A component gets renamed. An input property changes its type. A deprecated pattern is finally removed.
Without schematics, every breaking change becomes a migration document that 30 teams have to read, interpret, and execute manually — at different times, with different levels of care, producing inconsistent results.
With schematics, the change ships as code — and the CLI applies it automatically.
When @acme/ui publishes a new version, consumers run ng update @acme/ui. The CLI reads the version delta, runs the applicable migration schematics, and writes every change to disk. The team reviews a diff and commits. Here is what that looks like across different types of change:
Non-breaking change — new required input with a safe default. @acme/ui adds a variant input to AcmeButtonComponent with a default of 'primary'. The migration schematic scans all usages of <acme-button> in template files across the workspace and adds variant="primary" explicitly — making the implicit default visible and preparing the codebase for when the default is eventually removed.
Breaking change — renamed component selector. acme-data-grid is renamed to acme-table as part of a design system consolidation. The migration schematic scans every .html template file for <acme-data-grid and </acme-data-grid>, rewrites the tags, and updates the corresponding import paths in module files. Every consumer workspace is updated consistently in under a minute
The underlying schematic logic — reading files from the Tree, applying transformations, writing back — is identical to what we built in this article. What differs is the delivery mechanism: these schematics are registered in migrations.json and triggered automatically by ng update rather than invoked manually via ng generate.
🔔 Stay tuned: We cover
ng updatemigrations —migrations.json, version-gating, and TypeScript AST transformations — in depth in Part 4 of this series.
Platform-wide operational changes via schematics
Beyond code changes, schematics can drive operational and infrastructure changes across an entire estate of Angular repositories. This use case is underused and underappreciated — and it is a perfect demonstration of why chain exists.
Consider a real situation: your organisation moves from Bitbucket to GitHub, the DevOps team has issued a new standard CI configuration, the security team has mandated a SECURITY.md and CODEOWNERS file in every repo, and the platform is moving to a new secret management provider. In isolation, each of these is a ticket raised against 30 teams. Together, they are one schematic.
The platform team publishes @acme/platform-tools and each team runs a single command:
bash
ng generate @acme/platform-tools:repo-standardise
Under the hood, the factory chains every task 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 ]); }
Each Rule does one thing. Each is independently readable, testable, and skippable via a flag if a particular team doesn’t need it. The entire operation is previewed first with --dry-run, and because every write goes through the virtual Tree, either all of it commits or none of it does — no partially-updated repositories.
One command. One diff to review. Every repository on the platform reaches a known-good state. What would have been 30 separate Jira tickets across 6 weeks is a 30-second ng generate run per team.
🔔 Stay tuned: When a schematic also needs to install npm dependencies or perform first-time package setup,
ng addis the right mechanism — covered in Part 3. For changes that must fire automatically when teams upgrade a package version,ng updateis the answer — covered in Part 4.
ng generate vs ng update — Choosing the right delivery mechanism
A question that comes up frequently once teams start writing schematics seriously: should this be an ng generate or an ng update? The schematic logic — Tree reads, file transformations, overwrites — is often identical. What differs is entirely about how and when the change is triggered, and who controls that.
Understanding the distinction prevents the mistake we corrected earlier in this article: reaching for ng generate to deliver a version migration that properly belongs to 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
A few practices that keep a shared schematics package from becoming a burden:
- Keep it versioned alongside your component library. When the library changes, the schematic and a migration (Part 4) ship together.
- Test every schematic. Incorrect generated code at scale is far worse than no schematic. Part 5 of this series covers
SchematicTestRunnerandUnitTestTreein full. - Write the
descriptionfields inschema.jsonas if they are the docs — for most developers on your team,ng generate acme: --helpwill be the only documentation they read.
Summary & What’s Next
This example was deliberately kept to its minimum working form. Four files, one dependency, one concept per section. What we built:
- A schematics workspace wired to the Angular CLI via
package.json - A
collection.jsonthat registers the schematic by name - A
schema.jsonthat defines options, defaults, and interactive prompts - A factory function that applies templates to a target path
- Three template files that produce a complete, consistent component
From here, extending is straightforward. Want to add the component to an existing module automatically? Add a second Rule to the chain. The foundation is solid.
In Part 3 we shift from generating code to installing libraries. We’ll build an ng add schematic — the kind that fires when someone runs ng add my-ui-library for the first time — covering dependency installation, angular.json configuration, and automatic module wiring.
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 |
Built with Angular v21 · @angular-devkit/schematics · @angular-devkit/schematics-cli · @angular-devkit/core