Angular Schematics are powerful tools for transforming your codebase. They are the same technology that powers ng new, ng generate component, and ng add. By creating your own, you can eliminate repetitive tasks, enforce architectural patterns, and ensure your team builds features in a consistent and predictable way.
This guide will walk you through building a custom schematic from scratch. Our goal is to create a schematic that generates a complete “feature” module, including:
- An Angular Module (.module.ts)
- A Component (.component.ts, .html, .scss, .spec.ts)
- A Service (.service.ts, .spec.ts)
- A routing configuration file (-routing.module.ts)
- Automatic registration of the new feature route in the main app-routing.module.ts.
This automates significant boilerplate and enforces the convention of using feature modules with their own routing.
Core Concepts You Need to Understand
Tree
The most important concept. A Tree is a virtual representation of your filesystem. Schematics don’t write directly to your disk; they operate on this Tree. They can read, create, update, and delete files within it. Only when the schematic run is successful are the changes from the Tree applied to your real filesystem. This makes operations safe and atomic.
Rule
A Rule is a function that takes a Tree and returns a new Tree. This is the heart of a schematic’s logic. You chain together Rules to perform a series of transformations (e.g., a rule to create files from a template, a rule to modify an existing module, etc.).
SchematicContext
An object that provides context and utilities for a Rule, such as logging and access to the schematic’s collection information.
collection.json
A manifest file that describes your set of schematics. It maps a schematic name (e.g., create-feature) to the factory function that executes it and points to a schema file for defining command-line options.
Templates
Schematics use special template files to generate dynamic output. You can use placeholders like <%= variableName %> that get replaced with the options provided by the user.
Step-by-Step Practical Example: ng generate feature
Step 1: Set Up the Schematics Project
First, you need the Schematics CLI.
npm install -g @angular-devkit/schematics-cli
Now, create a new blank schematics project.
schematics blank --name=custom-schematics
cd custom-schematics
npm install
Your project structure will look like this:
custom-schematics/
├── src/
│ ├── collection.json # The manifest for your schematics
│ └── custom-schematics/ # A sample schematic
│ ├── index.js
│ └── index_spec.js
└── package.json
Let’s rename the sample schematic to feature and convert the files to TypeScript.
mv src/custom-schematics src/feature
mv src/feature/index.js src/feature/index.ts
mv src/feature/index_spec.js src/feature/index_spec.ts
Step 2: Define the Schematic in collection.json
This file is the entry point for the Angular CLI. Update src/collection.json to describe our new feature schematic.
{ "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "feature": { "description": "Generates a new feature module with a component, service, and routing.", "factory": "./feature/index#feature", "schema": "./feature/schema.json" } }
}
- “feature”: The name of our schematic (ng generate feature …).
- “factory”: Points to the main factory function (feature) inside index.ts.
- “schema”: Points to a JSON schema file where we’ll define the available command-line options.
Step 3: Define the User Options in schema.json
Create a new file src/feature/schema.json. This file defines the parameters our schematic will accept, like –name.
{ "$schema": "http://json-schema.org/schema", "id": "FeatureSchematic", "title": "Feature Schematic", "type": "object", "properties": { "name": { "type": "string", "description": "The name of the feature.", "$default": { "$source": "argv", "index": 0 }, "x-prompt": "What is the name of the feature?" }, "path": { "type": "string", "description": "The path to create the feature in.", "default": "app/features" } }, "required": [ "name" ]
}
- name: The feature’s name (e.g., “orders”, “profile”).
- $source: “argv”, index: 0 means the first argument passed in the command line will be used as the name (e.g., ng g feature my-feature).
- x-prompt provides a nice interactive prompt if the name isn’t supplied.
- path: Where to place the new feature folder. We default it to app/features.
We also need a schema.d.ts file to get TypeScript typings for our options. Create src/feature/schema.d.ts:
export interface Schema { /** * The name of the feature. */ name: string; /** * The path to create the feature in. */ path: string;
}
Step 4: Create the Template Files
This is the boilerplate we want to generate. Create a files folder inside src/feature. The special __name@dasherize__ syntax will be replaced with the dasherized version of the name option (e.g., “my-feature”).
src/feature/files/
└── __name@dasherize__/ ├── __name@dasherize__.component.html ├── __name@dasherize__.component.scss ├── __name@dasherize__.component.spec.ts ├── __name@dasherize__.component.ts ├── __name@dasherize__.module.ts ├── __name@dasherize__-routing.module.ts
└── __name@dasherize__.service.ts
Example Template File:
Notice the use of <%= … %> templating syntax. The classify and dasherize functions are helpers we’ll use in our logic.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { <%= classify(name) %>RoutingModule } from './<%= dasherize(name) %>-routing.module';
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';
import { <%= classify(name) %>Service } from './<%= dasherize(name) %>.service';
@NgModule({ declarations: [ <%= classify(name) %>Component ], imports: [ CommonModule, <%= classify(name) %>RoutingModule ], providers: [ <%= classify(name) %>Service ]
})
export class <%= classify(name) %>Module { }
Example Template File:
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { <%= classify(name) %>Component } from './<%= dasherize(name) %>.component';
const routes: Routes = [{ path: '', component: <%= classify(name) %>Component }];
@NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule]
})
export class <%= classify(name) %>RoutingModule { }
Step 5: Write the Main Schematic Logic in index.ts
This is where everything comes together. Replace the content of src/feature/index.ts with the following:
import { Rule, SchematicContext, Tree, apply, url, template, move, chain, mergeWith,
} from '@angular-devkit/schematics';
import { strings } from '@angular-devkit/core';
import { Schema } from './schema';
import { addRouteDeclarationToNgModule } from '@schematics/angular/utility/ast-utils';
import { InsertChange } from '@schematics/angular/utility/change';
import * as ts from 'typescript';
// Function to add the new route to app-routing.module.ts
function addRouteToNgModule(options: Schema): Rule { return (tree: Tree, _context: SchematicContext) => { const appRoutingModulePath = 'src/app/app-routing.module.ts'; const featureName = options.name; const featurePath = `${options.path}/${strings.dasherize(featureName)}/${strings.dasherize(featureName)}.module`; const text = tree.read(appRoutingModulePath); if (!text) { _context.logger.error(`Could not find ${appRoutingModulePath}.`); return tree; } const sourceText = text.toString('utf-8'); const sourceFile = ts.createSourceFile( appRoutingModulePath, sourceText, ts.ScriptTarget.Latest, true ); const route = `{ path: '${strings.dasherize(featureName)}', loadChildren: () => import('./features/${strings.dasherize(featureName)}/${strings.dasherize(featureName)}.module').then(m => m.${strings.classify(featureName)}Module) }`; // Use AST utils to find the routes array and insert the new route const changes = addRouteDeclarationToNgModule( sourceFile, appRoutingModulePath, route ); const recorder = tree.beginUpdate(appRoutingModulePath); for (const change of changes) { if (change instanceof InsertChange) { recorder.insertLeft(change.pos, change.toAdd); } } tree.commitUpdate(recorder); return tree; };
}
// Main factory function
export function feature(_options: Schema): Rule { return (_tree: Tree, _context: SchematicContext) => { const templateSource = apply(url('./files'), [ template({ ..._options, ...strings, // Provides classify, dasherize, etc. to templates }), move(_options.path), ]); // Chain the two rules together return chain([ mergeWith(templateSource), addRouteToNgModule(_options) ]); };
}
Explanation of the logic:
Function / Concept | Purpose / Explanation |
function | Main schematic factory where the rule chain is defined. |
url(‘./files’) | Reads template files from the ./files directory. These are the files to scaffold. |
template({…}) | Replaces placeholders like <%= %> in the templates using user inputs and utility functions (classify, dasherize). |
move(…) | Moves generated files to the path specified by the user (typically project structure). |
chain([…]) | Executes multiple Rules in order. Used here to generate files and then modify routing module. |
Rule (custom) | A custom rule to edit an existing file (app-routing.module.ts). |
Reads app-routing.module.ts | Accesses the routing module to inject new route information. |
Uses TypeScript AST | Uses the Abstract Syntax Tree to safely parse and modify code (more accurate than regex). |
addRouteDeclarationToNgModule | Utility from @schematics/angular to help inject new routes into the NgModule. |
InsertChange | A type of change object that represents text insertion. It’s applied to the file via the Tree. |
Step 6: Build and Test the Schematic
1. Build the Schematic
npm run build
2. Link for Local Testing:
From inside your custom-schematics directory:
npm link
Then, in your test Angular project:
cd ../path/to/your/angular-app
# Link the global schematic to this project
npm link custom-schematics
3. Run the Schematic :
Now you can run your custom schematic just like any other
ng generate custom-schematics:feature orders
Or, if you set it as the default collection in angular.json :
ng generate feature profile --path=app/user
After running, you will see a new orders folder inside app/features, fully populated with your component, service, and modules. More importantly, your app-routing.module.ts will have been automatically updated with the new lazy-loaded route, saving you time and preventing errors.
Final Words
With your custom Angular schematic ready, generating feature modules is quick and error-free. It includes routing, services, and components, streamlines development, enforces consistency, and saves time. This approach is especially useful for teams or businesses looking to hire Angular developers and maintain scalable, well-structured projects.