cyberangles guide

Custom Angular Schematics: Automating Your Workflow

As Angular applications grow in complexity, developers often find themselves repeating tedious tasks: creating feature modules with specific structures, generating components with predefined templates, or enforcing project-wide conventions. These repetitive actions not only waste time but also introduce inconsistencies—especially in large teams. Enter **Angular Schematics**: a powerful tool from the Angular DevKit that automates code generation, modification, and scaffolding. While Angular CLI uses schematics internally (e.g., `ng generate component`), custom schematics let you tailor workflows to your team’s exact needs. In this blog, we’ll demystify schematics, walk through building a practical custom schematic, and show you how to integrate it into your development pipeline.

Table of Contents

  1. What Are Angular Schematics?
  2. Why Build Custom Schematics?
  3. Setting Up Your Schematic Project
  4. Understanding Schematic Structure
  5. Creating Your First Custom Schematic
  6. Testing the Schematic
  7. Integrating the Schematic into Your Workflow
  8. Advanced Schematic Concepts
  9. Conclusion
  10. References

What Are Angular Schematics?

Angular Schematics are part of the Angular DevKit, a set of libraries for building Angular tools. At their core, schematics are code generators and transformers that operate on a virtual file system (the Tree). They can:

  • Generate new files (e.g., components, modules).
  • Modify existing files (e.g., update imports, add routes).
  • Enforce project conventions (e.g., naming standards, folder structures).

Key Concepts:

  • Tree: A read-only virtual file system that tracks changes (additions, deletions, modifications).
  • Rule: A function that takes a Tree and returns a new Tree with changes. Schematics are composed of rules.
  • Source: A provider of files (e.g., local templates, external schematics).
  • SchematicContext: Contains metadata like the schematic collection, logger, and options.

Why Custom Schematics?

While Angular CLI provides built-in schematics (e.g., component, module), custom schematics offer team-specific automation:

  • Consistency: Enforce project standards (e.g., “all feature modules must include a service and routing”).
  • Time Savings: Eliminate manual steps (e.g., generating a feature module, then adding a component, service, and tests).
  • Scalability: Adapt to your project’s unique architecture (e.g., Nx monorepos, micro-frontends).
  • Integration: Compose with existing schematics to extend functionality (e.g., “run the Angular module schematic, then add custom logging”).

Setting Up Your Schematic Project

To build a custom schematic, you’ll need the Angular DevKit Schematics CLI. Let’s set up a project:

Step 1: Install Dependencies

First, install the schematics CLI globally:

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

Step 2: Create a New Schematic

Generate a blank schematic project (replace my-schematics with your project name):

schematics blank --name=my-schematics  
cd my-schematics  

Step 3: Project Structure

Your project will look like this:

my-schematics/  
├── src/  
│   └── my-schematics/        # Schematic logic  
│       ├── files/            # Template files (optional)  
│       ├── index.ts          # Main schematic logic  
│       └── schema.json       # Defines schematic options  
├── collection.json           # Schematic collection config  
├── package.json              # Dependencies and scripts  
└── tsconfig.json             # TypeScript config  

Step 4: Install Dependencies

Install required packages for development:

npm install @angular-devkit/core @angular-devkit/schematics typescript @types/node @types/jasmine --save-dev  

Understanding Schematic Structure

Let’s break down the key files in your schematic:

1. collection.json

Defines your schematic collection (a group of schematics). It maps schematic names to their implementations:

{  
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",  
  "schematics": {  
    "my-schematics": {  # Schematic name (used in `ng generate my-schematics:my-schematics`)  
      "description": "A blank schematic.",  
      "factory": "./my-schematics/index#mySchematics",  # Path to the schematic function  
      "schema": "./my-schematics/schema.json"  # Path to options schema  
    }  
  }  
}  

2. schema.json

Defines the options users can pass to your schematic (e.g., --name=user). Example:

{  
  "$schema": "http://json-schema.org/schema",  
  "id": "MySchematic",  
  "title": "My Schematic",  
  "type": "object",  
  "properties": {  
    "name": {  # Option: --name  
      "type": "string",  
      "description": "The name of the feature."  
    },  
    "path": {  # Option: --path (where to generate files)  
      "type": "string",  
      "default": "src/app",  
      "description": "The path to generate the feature."  
    }  
  },  
  "required": ["name"]  # `name` is mandatory  
}  

3. index.ts

The main logic file. It exports a function that returns a Rule (the transformation to apply). Example:

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

export function mySchematics(_options: any): Rule {  
  return (tree: Tree, context: SchematicContext) => {  
    context.logger.info(`Generating feature: ${_options.name}`);  
    // Add logic here (e.g., generate files, modify existing ones)  
    return tree;  
  };  
}  

4. files/ Directory (Optional)

Contains template files (e.g., .ts, .html) used to generate code. Templates use EJS-like syntax (e.g., <%= name %> for variables).

Creating Your First Custom Schematic

Let’s build a practical schematic: feature—generates a feature module with:

  • A feature module (user.module.ts).
  • A component (user.component.ts).
  • A service (user.service.ts).
  • Optional routing.

Step 1: Update collection.json

Add the feature schematic to collection.json:

{  
  "schematics": {  
    "feature": {  # New schematic name: `ng generate my-schematics:feature`  
      "description": "Generates a feature module with component, service, and routing.",  
      "factory": "./feature/index#feature",  
      "schema": "./feature/schema.json"  
    }  
  }  
}  

Step 2: Create the feature Schematic

Create a src/feature directory with schema.json, index.ts, and files/:

mkdir -p src/feature/files  
touch src/feature/schema.json src/feature/index.ts  

Step 3: Define Options in schema.json

Update src/feature/schema.json to accept options:

{  
  "$schema": "http://json-schema.org/schema",  
  "id": "FeatureSchema",  
  "title": "Feature Options",  
  "type": "object",  
  "properties": {  
    "name": {  
      "type": "string",  
      "description": "Name of the feature (e.g., 'user').",  
      "$default": { "$source": "argv", "index": 0 }  # Default to first CLI argument  
    },  
    "path": {  
      "type": "string",  
      "default": "src/app/features",  
      "description": "Path to generate the feature."  
    },  
    "hasRouting": {  
      "type": "boolean",  
      "default": true,  
      "description": "Whether to include routing."  
    }  
  },  
  "required": ["name"]  
}  

Step 4: Write the Schematic Logic (index.ts)

Use Angular’s built-in module schematic to generate the module, then add custom files. Update src/feature/index.ts:

import { Rule, SchematicContext, Tree, apply, url, template, move, chain, externalSchematic } from '@angular-devkit/schematics';  
import { strings } from '@angular-devkit/core';  

// Interface for options (matches schema.json)  
interface FeatureOptions {  
  name: string;  
  path: string;  
  hasRouting: boolean;  
}  

export function feature(options: FeatureOptions): Rule {  
  const { name, path, hasRouting } = options;  
  const dasherizedName = strings.dasherize(name);  // Convert "UserProfile" to "user-profile"  
  const classifiedName = strings.classify(name);   // Convert "user-profile" to "UserProfile"  

  // Rule 1: Generate Angular module (using built-in `module` schematic  
  const moduleRule = externalSchematic('@schematics/angular', 'module', {  
    name: dasherizedName,  
    path,  
    routing: hasRouting,  
    flat: false,  // Create a subdirectory for the module  
  });  

  // Rule 2: Generate component, service, and other files from templates  
  const templateRule = apply(url('./files'), [  
    // Replace template variables (e.g., <%= name %>) with options  
    template({  
      ...strings,  // Provide string utilities (dasherize, classify, etc.)  
      name: dasherizedName,  
      classifiedName,  
    }),  
    // Move files to the target path (e.g., src/app/features/user)  
    move(`${path}/${dasherizedName}`),  
  ]);  

  // Run both rules sequentially  
  return chain([moduleRule, templateRule]);  
}  

Step 5: Create Template Files

Add templates to src/feature/files/ to generate the component and service.

__name@dasherize__.component.ts.template

import { Component } from '@angular/core';  

@Component({  
  selector: 'app-<%= name %>',  
  template: `  
    <h2><%= classifiedName %> Component</h2>  
  `,  
})  
export class <%= classifiedName %>Component {}  

__name@dasherize__.service.ts.template

import { Injectable } from '@angular/core';  

@Injectable({ providedIn: 'root' })  
export class <%= classifiedName %>Service {  
  constructor() {  
    console.log('<%= classifiedName %>Service initialized');  
  }  
}  

Step 6: Build the Schematic

Compile TypeScript to JavaScript:

npm run build  # Runs `tsc` (defined in package.json)  

Testing the Schematic

To test locally:

Link your schematic to npm so it’s available globally:

npm link  

Step 2: Generate the Feature in an Angular App

Create a test Angular app and use your schematic:

ng new test-app  
cd test-app  
npm link my-schematics  # Link to your schematic project  

# Generate the feature (e.g., name=user, path=src/app/features)  
ng generate my-schematics:feature --name=user --path=src/app/features  

Expected Output

You’ll see:

src/app/features/user/  
├── user.module.ts  
├── user.component.ts  
├── user.service.ts  
└── user-routing.module.ts (if --hasRouting=true)  

Integrating the Schematic into Your Workflow

Option 1: Local Development

Use npm link to test schematics locally (as shown above).

Option 2: Publish to npm

Share with your team by publishing to npm:

npm publish  # Ensure you’re logged in with `npm login`  

Then install in projects:

npm install my-schematics --save-dev  
ng generate my-schematics:feature --name=user  

Option 3: Integrate with Angular CLI

Add your schematic collection to angular.json for easier access:

{  
  "cli": {  
    "schematicCollections": ["my-schematics", "@schematics/angular"]  
  }  
}  

Now you can run:

ng generate feature --name=user  # No need for `my-schematics:` prefix  

Advanced Schematic Concepts

Composing Schematics

Use chain() to run multiple rules (e.g., generate a module, then add a route to app-routing.module.ts):

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

function addRouteToAppRouting(options: any): Rule {  
  return (tree: Tree) => {  
    // Modify src/app/app-routing.module.ts to add a route  
    const routeFile = tree.read('src/app/app-routing.module.ts')!.toString();  
    const newRoute = `{ path: '${options.name}', loadChildren: () => import('./features/${options.name}/${options.name}.module').then(m => m.${options.classifiedName}Module) },`;  
    const updatedContent = routeFile.replace('const routes: Routes = [', `const routes: Routes = [\n  ${newRoute}`);  
    tree.overwrite('src/app/app-routing.module.ts', updatedContent);  
    return tree;  
  };  
}  

export function feature(options: any): Rule {  
  return chain([moduleRule, templateRule, addRouteToAppRouting(options)]);  
}  

Validating Options

Use schema.json to enforce validation (e.g., name must be lowercase):

{  
  "properties": {  
    "name": {  
      "type": "string",  
      "pattern": "^[a-z-]+$",  # Enforce lowercase with dashes  
      "description": "Name must be lowercase with dashes (e.g., 'user-profile')."  
    }  
  }  
}  

Migrating Code

Use schematics to automate code updates (e.g., replace deprecated APIs):

import { visitTemplateFiles } from '@angular-devkit/schematics';  

export function migrate(): Rule {  
  return visitTemplateFiles((content, path) => {  
    // Replace `oldApi()` with `newApi()` in all .ts files  
    return content.replace(/oldApi\(\)/g, 'newApi()');  
  });  
}  

Conclusion

Custom Angular Schematics are a game-changer for automating repetitive tasks and enforcing consistency. By composing existing schematics and adding custom templates, you can tailor your workflow to your project’s needs. Start small (e.g., a feature generator) and expand to migrations or complex scaffolding—your team will thank you!

References