24 Mar 2026
11 min

Angular Schematics Deep Dive — Part 2 – ng generate

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:

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

  1. Setting Up the Workspace
  2. Project Structure
  3. collection.json — Registering the Schematic
  4. schema.json — Defining Options
  5. The Factory Function
  6. The strings Utility — What Every Function Does
  7. Decoding __name@dasherize__ — The Filename Template Syntax
  8. The File Templates
  9. Building and Running
  10. Before & After
  11. Why This Matters — Use Cases and Enterprise Standardisation
  12. 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 to name, so ng generate my-org-schematics:component my-button works without typing -name.
  • "x-prompt" makes the CLI ask interactively if name is not provided.
  • prefix defaults 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';
FunctionOutputExampleUse case
dasherizekebab-caseUserCarduser-cardFile names, selectors, import paths
classifyPascalCaseuser-cardUserCardClass names, module names
camelizecamelCaseuser-carduserCardVariable names, constructor params
underscoresnake_caseUserCarduser_cardConfig keys, backend interop
capitalizeFirst letter upuserCardUserCardTitles, first character only
decamelizespace separatedUserCarduser cardHuman-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 componentapp-my-button
Our custom schematicacme-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 update migrations — 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 add is the right mechanism — covered in Part 3. For changes that must fire automatically when teams upgrade a package version, ng update is 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 generateng update
TriggerManual — developer runs the command explicitlyAutomatic — CLI runs it as part of a package version upgrade
Version awarenessNone — no knowledge of current or target versionFull — runs only migrations applicable to the version delta
Registered incollection.jsonmigrations.json
IdempotencyDeveloper’s responsibility to guardCLI guarantees — each migration runs exactly once per version
Consumer controlExplicit opt-in — developer decides when to runImplicit — fires automatically on ng update
Best forOn-demand scaffolding, one-off operational tasks, opt-in changesLibrary 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 SchematicTestRunner and UnitTestTree in full.
  • Write the description fields in schema.json as if they are the docs — for most developers on your team, ng generate acme: --help will 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.json that registers the schematic by name
  • A schema.json that 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

PartTopicStatus
Part 1Understanding Angular Schematics — Architecture & Core Concepts✅ Published
Part 2Creating Custom Generators with ng generateYou are here
Part 3Building Installation Schematics with ng add🔜 Coming Soon
Part 4Writing Migration Schematics with ng update🔜 Coming Soon
Part 5Testing 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

Share this post

Sign up for our newsletter

Stay up-to-date with the trends and be a part of a thriving community.