Angular 124 🅰️ Angular Schematics and Custom Generators
Angular Schematics is the scaffolding engine behind every ng generate command. When you run ng generate component, ng add, or ng update, a schematic executes: it reads a template, applies transformations, writes files into the project’s virtual file tree, and returns the modified tree. The same engine is available to you, which means you can encode your team’s conventions — a specific folder structure, a Storybook file alongside every component, a CRUD service with a fixed set of methods — into a command that anyone can run.
The framework is built around three concepts: the Tree, which is a virtual representation of the project’s files; the Rule, which is a function that transforms the Tree; and the schematic factory, which returns a Rule. This functional design allows schematics to compose, chain, and test without touching the real file system. A schematic can be packaged in a library, published to npm, and consumed by any Angular workspace.
This chapter covers creating a schematic collection, defining the schema and options, writing factory functions and rules, using templates for file generation, testing schematics with the SchematicTestRunner, and publishing the collection to npm.
Key point: A schematic is a Rule that transforms a Tree. The factory function receives the schema options and returns a Rule. Templates generate files, apply and applyTemplates process them, and mergeWith merges the result into the Tree. Test schematics with SchematicTestRunner and a UnitTestTree. Publish the built collection to npm under the schematics key in package.json.
Why schematics exist
The convention problem. Every team has conventions: where components live, how services are named, which files accompany a feature. These conventions are not enforced by Angular, and they drift over time. A schematic encodes the convention and applies it consistently, so every generated file matches the team’s standard.
The boilerplate problem. A CRUD service has a fixed structure: imports, an injected HttpClient, a set of methods for create, read, update, and delete. Writing this by hand is tedious and error-prone. A schematic generates the boilerplate and leaves the implementation to the developer.
The migration problem. When an Angular version changes an API, the ng update command runs schematics that rewrite the project’s source files to use the new API. The same mechanism is available for internal migrations, such as updating a library’s import paths or renaming a deprecated service.
The integration problem. The ng add command runs a schematic that installs a library and configures the project to use it: adding imports, updating configuration, and generating starter code. A library that ships a well-designed ng-add schematic is easier to adopt than one that requires manual setup.
The testing problem. A schematic that generates files is testable without touching the file system. The SchematicTestRunner creates a UnitTestTree in memory, runs the schematic, and returns the modified tree for assertions. This makes it possible to verify that a schematic creates the right files with the right content.
a. Setting up a schematic collection
A schematic collection is a directory with a collection.json file that lists the schematics and a folder for each schematic’s implementation. The @schematics/angular collection is the reference example.
To create a collection from scratch, use the schematics CLI:
npm install -g @angular-devkit/schematics-cli
schematics blank --name=my-schematics
cd my-schematics
The blank command generates a project with a collection.json, a package.json, and an src folder containing one schematic. The collection.json is the manifest that maps schematic names to their factory functions.
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"my-schematic": {
"description": "A blank schematic",
"factory": "./my-schematic/index#mySchematic"
}
}
}
The factory value has two parts: the path to the module and the name of the exported function, separated by #. The schematics CLI provides commands to add new schematics to the collection, and the build command compiles the TypeScript to JavaScript for publication.
b. Defining the schema and options
Each schematic has a schema.json file that defines its options. The schema uses JSON Schema syntax and controls the command-line flags, the prompts shown to the user, and the validation of the input.
{
"$schema": "http://json-schema.org/schema",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"default": "world",
"x-prompt": "What is your name?"
},
"useColor": {
"type": "boolean",
"default": false,
"x-prompt": "Would you like the response in color?"
}
},
"required": ["name"]
}
The x-prompt field adds an interactive prompt. The type is inferred from the property’s schema: a string becomes a text input, a boolean becomes a confirmation, and an enum becomes a list .
For a service schematic, the schema includes the service name, the path, and the project:
{
"$schema": "http://json-schema.org/schema",
"properties": {
"name": {
"type": "string",
"description": "The name of the service.",
"$default": { "$source": "argv", "index": 0 }
},
"path": {
"type": "string",
"format": "path",
"description": "The path to create the service.",
"default": "src/app"
},
"project": {
"type": "string",
"description": "The name of the project.",
"$default": { "$source": "projectName" }
}
},
"required": ["name"]
}
The $default with $source: "projectName" automatically supplies the current project name when the schematic runs inside a workspace .
The schema is paired with a TypeScript interface that types the options:
export interface Schema {
name: string;
path?: string;
project?: string;
}
c. Writing the factory function and rules
The factory function receives the schema options and returns a Rule. A Rule is a function that takes a Tree and a SchematicContext and returns a Tree (or a Promise/Observable of one).
import { Rule, Tree, SchematicContext } from '@angular-devkit/schematics';
export function myService(options: Schema): Rule {
return (tree: Tree, _context: SchematicContext) => {
// modify the tree
tree.create(options.path + '/' + options.name + '.ts', '// content');
return tree;
};
}
The Tree is a virtual file system. Its methods include create, overwrite, delete, read, exists, and getDir. Changes to the Tree are not written to disk until the schematic completes successfully, which allows the framework to roll back on error.
For generation schematics, the apply, applyTemplates, move, and mergeWith utilities handle the common pattern of loading templates, substituting variables, and merging the result into the Tree.
import {
Rule, Tree, SchematicsException,
apply, url, applyTemplates, move,
chain, mergeWith,
} from '@angular-devkit/schematics';
import { strings, normalize, virtualFs, workspaces } from '@angular-devkit/core';
import { Schema as MyServiceSchema } from './schema';
export function myService(options: MyServiceSchema): Rule {
return (tree: Tree) => {
const templateSource = apply(url('./files'), [
applyTemplates({
...options,
classify: strings.classify,
dasherize: strings.dasherize,
}),
move(normalize(options.path ?? 'src/app')),
]);
return chain([mergeWith(templateSource)]);
};
}
The apply function applies a set of rules to a source. The applyTemplates rule substitutes placeholders in the templates using the provided context. The move rule relocates the generated files. The mergeWith rule merges the generated tree into the main tree. The chain function combines multiple rules into one .
d. Using templates for file generation
Templates live in a files folder inside the schematic directory. The file names and contents support placeholders that are substituted when the schematic runs.
A template file is named with a placeholder prefix:
files/__name@dasherize__.service.ts.template
The __name@dasherize__ part is replaced by the name option, transformed by the dasherize function. For a schematic run with name: "userProfile", the generated file is named user-profile.service.ts.
The template content uses <%= %> for substitution and <% %> for logic:
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class <%= classify(name) %>Service {
private http = inject(HttpClient);
}
The classify function converts user-profile to UserProfile, producing UserProfileService. The dasherize function converts userProfile to user-profile .
The applyTemplates rule passes the options to the template engine, and the url('./files') function loads the templates from the file system into the Tree.
e. Composing with existing schematics
A custom schematic can call the Angular CLI’s own schematics using externalSchematic. This is useful when you want to generate a standard Angular artifact and then modify it.
import { chain, externalSchematic, Rule } from '@angular-devkit/schematics';
export function myComponent(options: any): Rule {
return chain([
externalSchematic('@schematics/angular', 'component', options),
// additional rules to modify the generated component
]);
}
The externalSchematic function runs another schematic and merges its result into the Tree. The @schematics/angular collection is the Angular CLI’s built-in collection, and its schematics include component, service, directive, pipe, guard, module, and many others .
For a CRUD service schematic, the pattern is to call the standard service schematic and then merge in a template that adds the CRUD methods :
export function crudService(_options: any): Rule {
return chain([
externalSchematic('@schematics/angular', 'service', _options),
mergeWith(templateSource),
]);
}
This composes the standard service generation with the custom template, so the developer gets both the standard Angular service structure and the CRUD boilerplate.
f. Testing schematics
Schematic tests run without the browser and without the Angular test runner. They use the SchematicTestRunner from @angular-devkit/schematics/testing and a UnitTestTree.
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { Tree } from '@angular-devkit/schematics';
import * as path from 'path';
const collectionPath = path.join(__dirname, '../collection.json');
describe('my-service', () => {
const runner = new SchematicTestRunner('schematics', collectionPath);
it('creates a service file', async () => {
const tree = await runner.runSchematic('my-service', { name: 'user' }, Tree.empty());
expect(tree.files).toContain('/src/app/user.service.ts');
});
});
The runSchematic method takes the schematic name, the options, and the input Tree. It returns a Promise of the modified Tree. The Tree.empty() function creates an empty tree for testing in isolation.
For schematics that depend on the Angular workspace, the test creates a workspace first using runExternalSchematic:
const workspaceTree = await runner.runExternalSchematic(
'@schematics/angular',
'workspace',
{ name: 'workspace', version: '20.0.0' }
);
const appTree = await runner.runExternalSchematic(
'@schematics/angular',
'application',
{ name: 'app', style: 'css' },
workspaceTree
);
const tree = await runner.runSchematic('my-service', { name: 'user' }, appTree);
The runExternalSchematic method runs a schematic from another collection, which is how the test sets up a realistic workspace .
Schematic tests should not run with ng test, because ng test runs in the browser and schematics run in Node.js. The Angular CLI’s schematics project provides a separate test configuration with its own tsconfig and Jasmine runner .
g. Packaging and publishing
A schematic collection is published to npm as part of a library or as a standalone package. The built JavaScript is what gets published, so the TypeScript must be compiled first.
The schematics CLI provides a build script that compiles the schematics folder:
npm run build
The package.json of the collection includes a schematics key that points to the built collection.json:
{
"name": "my-schematics",
"version": "1.0.0",
"schematics": "./dist/collection.json",
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "jasmine"
}
}
For a library that ships schematics alongside its components, the ng-packagr configuration includes the schematics field:
{
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/my-lib",
"lib": {
"entryFile": "src/public-api.ts"
},
"schematics": "./schematics/collection.json"
}
When the library is installed, the Angular CLI reads the schematics field and makes the collection available for ng generate, ng add, and ng update.
To use the schematic locally before publishing, run npm link in the collection directory and then use the schematic from another project:
cd my-schematics
npm run build
npm link
cd ../my-app
ng generate my-schematics:my-service user
The ng generate command accepts the collection name and the schematic name, separated by a colon.
Complete Example Session
# ============================================
# PART 1: CREATE A SCHEMATIC COLLECTION
# ============================================
npm install -g @angular-devkit/schematics-cli
schematics blank --name=my-schematics
cd my-schematics
// ============================================
// PART 2: COLLECTION.JSON
// ============================================
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"crud-service": {
"description": "Generate a CRUD HTTP service",
"factory": "./crud-service/index#crudService"
}
}
}
// ============================================
// PART 3: SCHEMA.JSON
// ============================================
{
"$schema": "http://json-schema.org/schema",
"properties": {
"name": {
"type": "string",
"description": "The name of the service.",
"$default": { "$source": "argv", "index": 0 }
},
"path": {
"type": "string",
"format": "path",
"default": "src/app"
},
"project": {
"type": "string",
"$default": { "$source": "projectName" }
}
},
"required": ["name"]
}
// ============================================
// PART 4: SCHEMA INTERFACE
// ============================================
export interface Schema {
name: string;
path?: string;
project?: string;
}
// ============================================
// PART 5: TEMPLATE FILE
// ============================================
// files/__name@dasherize__.service.ts.template
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' })
export class <%= classify(name) %>Service {
private http = inject(HttpClient);
apiUrl = '/api';
create(obj: unknown) { return this.http.post(this.apiUrl, obj); }
read() { return this.http.get(this.apiUrl); }
update(obj: unknown) { return this.http.put(this.apiUrl, obj); }
delete(id: string) { return this.http.delete(`${this.apiUrl}/${id}`); }
}
// ============================================
// PART 6: FACTORY FUNCTION
// ============================================
import {
Rule, Tree, apply, url, applyTemplates,
move, chain, mergeWith, externalSchematic,
} from '@angular-devkit/schematics';
import { strings, normalize } from '@angular-devkit/core';
import { Schema } from './schema';
export function crudService(options: Schema): Rule {
const templateSource = apply(url('./files'), [
applyTemplates({
...options,
classify: strings.classify,
dasherize: strings.dasherize,
}),
move(normalize(options.path ?? 'src/app')),
]);
return chain([
externalSchematic('@schematics/angular', 'service', options),
mergeWith(templateSource),
]);
}
// ============================================
// PART 7: UNIT TEST
// ============================================
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import { Tree } from '@angular-devkit/schematics';
import * as path from 'path';
const collectionPath = path.join(__dirname, '../collection.json');
describe('crud-service', () => {
const runner = new SchematicTestRunner('schematics', collectionPath);
it('creates a service file', async () => {
const tree = await runner.runSchematic(
'crud-service',
{ name: 'user', path: 'src/app' },
Tree.empty()
);
expect(tree.files).toContain('/src/app/user.service.ts');
});
});
# ============================================
# PART 8: BUILD AND LINK LOCALLY
# ============================================
npm run build
npm link
cd ../my-app
ng generate my-schematics:crud-service user
// ============================================
// PART 9: PACKAGE.JSON FOR PUBLISHING
// ============================================
{
"name": "my-schematics",
"version": "1.0.0",
"schematics": "./dist/collection.json",
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "jasmine"
}
}
# ============================================
# PART 10: PUBLISH
# ============================================
npm publish
These ten parts cover creating the collection, the collection.json, the schema.json, the schema interface, the template file, the factory function, the unit test, building and linking locally, the package.json for publishing, and publishing.
Quick Reference
Core Concepts
| Concept | Definition |
|---|---|
| Tree | Virtual file system representing the project |
| Rule | Function that transforms a Tree |
| Factory | Function that receives options and returns a Rule |
| Schematic | Named entry in a collection that maps to a factory |
Common Utilities
| Utility | Purpose |
|---|---|
apply | Apply rules to a source |
url | Load templates from a directory |
applyTemplates | Substitute template placeholders |
move | Relocate generated files |
mergeWith | Merge a tree into the main tree |
chain | Combine rules into one |
externalSchematic | Run another collection’s schematic |
Schema Options
| Option | Purpose |
|---|---|
type | Data type (string, boolean, number, enum) |
default | Default value |
description | Help text |
$default | Auto-populate from context |
x-prompt | Interactive prompt |
Prompt Types
| Property Type | Prompt Type |
|---|---|
boolean | Confirmation |
string | Input |
number | Input |
enum | List |
Testing
| API | Purpose |
|---|---|
SchematicTestRunner | Run schematics in tests |
UnitTestTree | Tree returned by the runner |
runSchematic | Run a schematic from the collection |
runExternalSchematic | Run a schematic from another collection |
Tree.empty() | Empty tree for isolated tests |
Packaging
| File | Purpose |
|---|---|
collection.json | Manifest of schematics |
schema.json | Options for one schematic |
package.json | schematics key points to built collection |
Best Practices
✅ Do This:
// Use applyTemplates for substitution
applyTemplates({ ...options, classify: strings.classify, dasherize: strings.dasherize })
// Chain rules for composition
chain([externalSchematic('@schematics/angular', 'service', options), mergeWith(templateSource)])
// Test with UnitTestTree
const tree = await runner.runSchematic('my-schematic', {}, Tree.empty());
// Use $default for project name
"$default": { "$source": "projectName" }
// Separate schematic tests from ng test
"schm:test": "npm run schm:build && jasmine schematics/**/*.spec.js"
❌ Don’t Do This:
// Modify files directly
tree.create('/absolute/path.ts', 'content'); // ❌ use move and relative paths
// Forget to return the tree
return (tree) => { tree.create(...); }; // ❌ no return
// Run schematic tests with ng test
ng test // ❌ schematics run in Node.js, not the browser
// Use inline functions for templates
// No template syntax // ❌ use <%= classify(name) %>
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| File not generated | Template not merged | Use mergeWith and chain |
| Placeholder not substituted | Missing applyTemplates | Add applyTemplates with context |
| Wrong file name | Missing dasherize | Use __name@dasherize__ |
| Test fails with UTF-8 error | Jest configuration issue | Use the schematic test runner, not ng test |
| Schematic not found | Collection not linked | Run npm link and use collection:name |
externalSchematic fails | Missing collection path | Verify the collection name and options |
| Build not published | Forgot npm run build | Build before npm publish |
Real-World Examples
1. Basic Schematic
export function mySchematic(options: any): Rule {
return (tree: Tree) => {
tree.create('hello.txt', 'Hello!');
return tree;
};
}
2. Template with Placeholder
// files/__name__.component.ts.template
export class <%= classify(name) %>Component {}
3. applyTemplates
applyTemplates({ ...options, classify: strings.classify, dasherize: strings.dasherize })
4. Merge Template
return chain([mergeWith(apply(url('./files'), [applyTemplates({ ...options })]))]);
5. External Schematic
externalSchematic('@schematics/angular', 'component', options)
6. Schema with Prompt
"name": {
"type": "string",
"x-prompt": "What is your name?"
}
7. Project Default
"$default": { "$source": "projectName" }
8. Unit Test
const tree = await runner.runSchematic('my-schematic', { name: 'test' }, Tree.empty());
expect(tree.files).toContain('/test.ts');
9. Local Link
npm link
ng generate my-schematics:my-schematic test
10. Publish
npm run build && npm publish
Visual
Schematic Architecture
┌──────────────────────────────────────────────────────────────┐
│ ng generate my-schematics:crud-service user │
│ │ │
│ ▼ │
│ collection.json → crud-service → factory │
│ │ │
│ ▼ │
│ Schema options: { name: 'user', path: 'src/app' } │
│ │ │
│ ▼ │
│ Factory returns a Rule │
│ │ │
│ ▼ │
│ chain([ │
│ externalSchematic('@schematics/angular', 'service', ...), │
│ mergeWith(templateSource) │
│ ]) │
│ │ │
│ ▼ │
│ Tree modified → files written to disk │
└──────────────────────────────────────────────────────────────┘
Template Processing
┌──────────────────────────────────────────────────────────────┐
│ files/__name@dasherize__.service.ts.template │
│ │ │
│ ▼ │
│ applyTemplates({ name, classify, dasherize }) │
│ │ │
│ ▼ │
│ user.service.ts │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ export class UserService { ... } │ │
│ └────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
Testing Flow
┌──────────────────────────────────────────────────────────────┐
│ SchematicTestRunner('schematics', collectionPath) │
│ │ │
│ ▼ │
│ runner.runSchematic('crud-service', options, Tree.empty()) │
│ │ │
│ ▼ │
│ UnitTestTree │
│ ├── /src/app/user.service.ts │
│ └── /src/app/user.service.spec.ts │
│ │ │
│ ▼ │
│ Assertions on the tree │
└──────────────────────────────────────────────────────────────┘
Composition with externalSchematic
┌──────────────────────────────────────────────────────────────┐
│ Custom Schematic │
│ │ │
│ ├── externalSchematic('@schematics/angular', 'service')│
│ │ └── Standard Angular service file │
│ │ │
│ └── mergeWith(templateSource) │
│ └── CRUD methods added to the service │
│ │
│ Result: standard service + CRUD boilerplate │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Schematic | Named generator in a collection |
| Tree | Virtual file system |
| Rule | Function that transforms a Tree |
| Factory | Function returning a Rule |
collection.json | Manifest of schematics |
schema.json | Options and prompts |
applyTemplates | Substitute template placeholders |
mergeWith | Merge generated tree |
externalSchematic | Run another collection’s schematic |
SchematicTestRunner | Test schematics in Node.js |
UnitTestTree | Tree returned by tests |
| Publishing | Build, then npm publish |
Key takeaways:
- Schematics are the engine behind
ng generate. They read templates, apply transformations, and write files into a virtual Tree. Understanding them means understanding how Angular scaffolds code. - The factory function receives options and returns a Rule. The Rule transforms the Tree. The
apply,applyTemplates,move, andmergeWithutilities handle the common generation pattern. - The schema defines the options and the prompts.
x-promptadds interactive questions, and$defaultauto-populates values like the project name. - Templates use
<%= %>and file name placeholders.classifyanddasherizetransform the input name into class names and file names. externalSchematiccomposes with the Angular CLI’s own schematics. A custom schematic can generate a standard Angular artifact and then modify it.- Test schematics with
SchematicTestRunner. Create aUnitTestTree, run the schematic, and assert on the files. Schematic tests run in Node.js, not the browser, so they do not useng test. - Publish the built collection to npm. The
package.jsonschematicskey points to the builtcollection.json. Libraries can ship schematics alongside their components.
Remember: Angular Schematics is the framework for encoding conventions into commands. A schematic is a function that transforms a virtual file tree, and the framework provides the utilities to load templates, substitute variables, and merge results. The schema defines what the user can configure, and the prompts make the schematic interactive. The same mechanism powers ng generate, ng add, and ng update, so understanding it means understanding how Angular itself scaffolds and migrates code. Whether you are generating a single component with a companion Storybook file or a full CRUD service with a fixed set of methods, the pattern is the same: define the schema, write the factory, provide the templates, and test the result with the SchematicTestRunner.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!