Skip to main content

What are Schematics?

Schematics are template-based code generators that support complex logic. They are the engine behind ng generate commands and ng add package installations. Schematics can create, modify, delete, and move files while applying rules and templates to scaffold or modify code.
Think of schematics as “code recipes” - they describe how to generate or transform code in a repeatable, configurable way.

How Schematics Work

Schematics operate on a virtual file system called a tree. Changes are staged in memory before being committed to disk, allowing for validation and rollback.

Tree

Virtual file system representing project structure

Rule

Transformation applied to the tree

Template

File template with placeholders

Action

Create, update, delete, or rename operations

Official Schematics

Angular provides built-in schematics for common tasks:

Code Generation Schematics

ng generate @schematics/angular:component user-profile
Generates:
  • Component TypeScript file
  • Component template (HTML)
  • Component styles (CSS/SCSS)
  • Component test file
Options:
  • --standalone: Create standalone component
  • --inline-template: Use inline template
  • --inline-style: Use inline styles
  • --skip-tests: Skip test file
  • --flat: Don’t create folder
  • --export: Export from module

Migration Schematics

Angular provides powerful migration schematics:
Convert components, directives, and pipes to standalone:
ng generate @angular/core:standalone
Three-phase migration:
1

Convert to Standalone

Convert declarations to standalone and update imports
2

Remove NgModules

Delete unnecessary NgModule classes
3

Update Bootstrap

Switch to bootstrapApplication API
Options:
  • mode: Migration mode (convert, prune, bootstrap)
  • path: Relative path to migrate
See the standalone migration README for details.
Migrate to new control flow syntax:
ng generate @angular/core:control-flow-migration
Transforms:
  • *ngIf@if
  • *ngFor@for
  • *ngSwitch@switch
Before:
<div *ngIf="show">Content</div>
<li *ngFor="let item of items">{{ item }}</li>
After:
@if (show) {
  <div>Content</div>
}
@for (item of items; track item) {
  <li>{{ item }}</li>
}
Migrate to signal-based APIs:Signal Inputs:
ng generate @angular/core:signal-input-migration
Signal Queries:
ng generate @angular/core:signal-queries-migration
Output Migration:
ng generate @angular/core:output-migration
NgStyle to Style:
ng generate @angular/core:ngstyle-to-style-migration
NgClass to Class:
ng generate @angular/core:ngclass-to-class-migration
Inject Migration:
ng generate @angular/core:inject-migration
Router Testing Module:
ng generate @angular/core:router-testing-module-migration
Cleanup Unused Imports:
ng generate @angular/core:cleanup-unused-imports
All migration schematics support a --path option to migrate specific directories.

Creating Custom Schematics

Build your own code generators for common patterns in your application.

Setup

1

Install Schematics CLI

npm install -g @angular-devkit/schematics-cli
2

Create Schematics Collection

schematics blank my-schematics
cd my-schematics
npm install
3

Project Structure

my-schematics/
├── src/
│   ├── my-schematic/
│   │   ├── index.ts           # Schematic logic
│   │   ├── schema.json        # Options schema
│   │   ├── schema.d.ts        # TypeScript interface
│   │   └── files/             # Templates
│   │       └── __name@dasherize__/
│   │           ├── __name@dasherize__.component.ts
│   │           └── __name@dasherize__.component.html
│   └── collection.json        # Collection metadata
├── package.json
└── tsconfig.json

Define Schema

schema.json:
{
  "$schema": "http://json-schema.org/schema",
  "$id": "MySchematic",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "The name of the component",
      "$default": {
        "$source": "argv",
        "index": 0
      }
    },
    "path": {
      "type": "string",
      "format": "path",
      "description": "The path to create the component",
      "visible": false
    },
    "project": {
      "type": "string",
      "description": "The name of the project"
    },
    "export": {
      "type": "boolean",
      "default": false,
      "description": "Export the component from the module"
    }
  },
  "required": ["name"]
}
schema.d.ts:
export interface Schema {
  name: string;
  path?: string;
  project?: string;
  export?: boolean;
}

Implement Schematic

index.ts:
import {
  Rule,
  SchematicContext,
  Tree,
  apply,
  url,
  template,
  mergeWith,
  move,
  chain,
  SchematicsException,
} from '@angular-devkit/schematics';
import { strings, normalize, experimental } from '@angular-devkit/core';
import { Schema } from './schema';

export function mySchematic(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    // Validate options
    if (!options.name) {
      throw new SchematicsException('Option "name" is required.');
    }

    // Get workspace
    const workspaceConfig = tree.read('/angular.json');
    if (!workspaceConfig) {
      throw new SchematicsException('Could not find Angular workspace configuration');
    }

    // Parse workspace
    const workspace: experimental.workspace.WorkspaceSchema = JSON.parse(
      workspaceConfig.toString()
    );

    // Determine project
    if (!options.project) {
      options.project = workspace.defaultProject;
    }

    const project = workspace.projects[options.project!];
    if (!project) {
      throw new SchematicsException(`Project "${options.project}" not found.`);
    }

    // Determine path
    if (options.path === undefined) {
      options.path = `${project.sourceRoot}/app`;
    }

    // Apply template
    const templateSource = apply(url('./files'), [
      template({
        ...strings,
        ...options,
      }),
      move(normalize(options.path as string)),
    ]);

    return chain([
      mergeWith(templateSource),
      // Add additional rules here
    ])(tree, context);
  };
}

Create Templates

files/__name@dasherize__/__name@dasherize__.component.ts:
import { Component } from '@angular/core';

@Component({
  selector: 'app-<%= dasherize(name) %>',
  templateUrl: './<%= dasherize(name) %>.component.html',
  styleUrls: ['./<%= dasherize(name) %>.component.<%= style %>'],
  standalone: true
})
export class <%= classify(name) %>Component {
  constructor() { }
}
files/__name@dasherize__/__name@dasherize__.component.html:
<div class="<%= dasherize(name) %>-container">
  <h2><%= classify(name) %> Component</h2>
  <p><%= dasherize(name) %> works!</p>
</div>

Template Functions

Angular provides utility functions for string transformation:
String Functions
FunctionInputOutputDescription
dasherizeMyComponentmy-componentConvert to kebab-case
classifymy-componentMyComponentConvert to PascalCase
camelizemy-componentmyComponentConvert to camelCase
capitalizehelloHelloCapitalize first letter
underscoreMyComponentmy_componentConvert to snake_case

Schematic Rules

Rules define transformations applied to the tree:

Basic Rules

import { Rule, Tree } from '@angular-devkit/schematics';

// Create a file
export function createFile(): Rule {
  return (tree: Tree) => {
    tree.create('new-file.ts', 'console.log("Hello");');
    return tree;
  };
}

// Read a file
export function readFile(): Rule {
  return (tree: Tree) => {
    const content = tree.read('existing-file.ts');
    if (content) {
      console.log(content.toString());
    }
    return tree;
  };
}

// Update a file
export function updateFile(): Rule {
  return (tree: Tree) => {
    const recorder = tree.beginUpdate('file.ts');
    recorder.insertLeft(0, '// File header\n');
    tree.commitUpdate(recorder);
    return tree;
  };
}

// Delete a file
export function deleteFile(): Rule {
  return (tree: Tree) => {
    tree.delete('old-file.ts');
    return tree;
  };
}

Chaining Rules

import { chain, Rule } from '@angular-devkit/schematics';

export function mySchematic(): Rule {
  return chain([
    createFile(),
    updateFile(),
    deleteFile(),
  ]);
}

Conditional Rules

import { Rule, SchematicContext } from '@angular-devkit/schematics';

export function conditionalRule(options: Schema): Rule {
  return (tree: Tree, context: SchematicContext) => {
    if (options.export) {
      context.logger.info('Exporting component...');
      // Add export logic
    }
    
    return tree;
  };
}

Advanced Features

Working with AST

Modify TypeScript files using Abstract Syntax Trees:
import * as ts from 'typescript';
import { InsertChange } from '@schematics/angular/utility/change';

function addImport(tree: Tree, filePath: string): Tree {
  const source = tree.read(filePath);
  if (!source) return tree;
  
  const sourceFile = ts.createSourceFile(
    filePath,
    source.toString(),
    ts.ScriptTarget.Latest,
    true
  );
  
  // Find where to insert import
  const allImports = sourceFile.statements.filter(
    node => ts.isImportDeclaration(node)
  );
  
  const lastImport = allImports[allImports.length - 1];
  const insertPos = lastImport ? lastImport.end + 1 : 0;
  
  // Create change
  const change = new InsertChange(
    filePath,
    insertPos,
    "\nimport { Component } from '@angular/core';\n"
  );
  
  // Apply change
  const recorder = tree.beginUpdate(filePath);
  recorder.insertLeft(change.pos, change.toAdd);
  tree.commitUpdate(recorder);
  
  return tree;
}

External Schematics

Call other schematics from your schematic:
import { externalSchematic, Rule, chain } from '@angular-devkit/schematics';

export function mySchematic(options: Schema): Rule {
  return chain([
    // Call Angular's component schematic
    externalSchematic('@schematics/angular', 'component', {
      name: options.name,
      standalone: true,
    }),
    // Add custom logic
    customRule(options),
  ]);
}

Task Scheduling

Schedule tasks to run after schematic completes:
import { Rule, SchematicContext } from '@angular-devkit/schematics';
import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks';

export function mySchematic(): Rule {
  return (tree: Tree, context: SchematicContext) => {
    // Schedule npm install
    context.addTask(new NodePackageInstallTask());
    
    return tree;
  };
}

Testing Schematics

Test schematics using the testing utilities:
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
import * as path from 'path';

describe('my-schematic', () => {
  const collectionPath = path.join(__dirname, '../collection.json');
  const runner = new SchematicTestRunner('schematics', collectionPath);
  
  let appTree: UnitTestTree;
  
  beforeEach(() => {
    appTree = runner.runExternalSchematic(
      '@schematics/angular',
      'workspace',
      { name: 'workspace', version: '14.0.0' }
    );
  });
  
  it('should create component files', async () => {
    const options = { name: 'test' };
    
    const tree = await runner.runSchematic('my-schematic', options, appTree);
    
    expect(tree.files).toContain('/test/test.component.ts');
    expect(tree.files).toContain('/test/test.component.html');
  });
  
  it('should have correct content', async () => {
    const options = { name: 'test' };
    
    const tree = await runner.runSchematic('my-schematic', options, appTree);
    const content = tree.readContent('/test/test.component.ts');
    
    expect(content).toContain('class TestComponent');
    expect(content).toContain('standalone: true');
  });
});

Publishing Schematics

1

Build the Package

npm run build
2

Test Locally

npm link
cd /path/to/test-project
npm link my-schematics
ng generate my-schematics:my-schematic test
3

Publish to npm

npm publish
4

Use in Projects

ng add my-schematics
ng generate my-schematics:my-schematic test

Collection Configuration

collection.json:
{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "my-schematic": {
      "description": "Generate a custom component",
      "factory": "./my-schematic/index#mySchematic",
      "schema": "./my-schematic/schema.json",
      "aliases": ["ms"],
      "hidden": false
    },
    "ng-add": {
      "description": "Add my-schematics to the project",
      "factory": "./ng-add/index#ngAdd",
      "schema": "./ng-add/schema.json"
    }
  }
}
factory
string
required
Path to schematic implementation and export name
schema
string
Path to JSON schema file defining options
aliases
string[]
Alternative names for the schematic
hidden
boolean
Hide from ng generate list

Best Practices

Always validate options and check for existing files:
if (!options.name) {
  throw new SchematicsException('Name is required');
}

if (tree.exists(targetPath)) {
  throw new SchematicsException(`File already exists: ${targetPath}`);
}
Provide helpful feedback:
context.logger.info('Creating component...');
context.logger.warn('This will overwrite existing files');
context.logger.error('Failed to create component');
context.logger.debug('Debug information');
Test with --dry-run flag:
ng generate my-schematics:my-schematic test --dry-run
Shows what would be generated without creating files.
Use normalize and join for cross-platform compatibility:
import { normalize, join } from '@angular-devkit/core';

const targetPath = normalize(join(options.path, options.name));

Next Steps

CLI Overview

Return to CLI overview

Builders

Learn about custom builders

Resources

Schematics Guide

Official schematics documentation

Angular Schematics

Official Angular schematics source

DevKit Schematics

Schematics DevKit source

Example Schematics

Angular Core migration schematics