{"id":2354,"date":"2025-09-25T13:21:04","date_gmt":"2025-09-25T13:21:04","guid":{"rendered":"https:\/\/www.cmarix.com\/qanda\/?p=2354"},"modified":"2026-02-05T11:59:04","modified_gmt":"2026-02-05T11:59:04","slug":"create-custom-angular-schematics-to-automate-boilerplate","status":"publish","type":"post","link":"https:\/\/www.cmarix.com\/qanda\/create-custom-angular-schematics-to-automate-boilerplate\/","title":{"rendered":"How do you Create a Custom Angular Schematic to Automate Boilerplate and Enforce Conventions?"},"content":{"rendered":"\n<p>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.<\/p>\n\n\n\n<p>This guide will walk you through building a custom schematic from scratch. Our goal is to create a schematic that generates a complete &#8220;feature&#8221; module, including:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>An Angular Module (.module.ts)<\/li>\n\n\n\n<li>A Component (.component.ts, .html, .scss, .spec.ts)<\/li>\n\n\n\n<li>A Service (.service.ts, .spec.ts)<\/li>\n\n\n\n<li>A routing configuration file (-routing.module.ts)<\/li>\n\n\n\n<li>Automatic registration of the new feature route in the main app-routing.module.ts.<\/li>\n<\/ul>\n\n\n\n<p>This automates significant boilerplate and enforces the convention of using feature modules with their own routing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Core Concepts You Need to Understand<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Tree<\/h3>\n\n\n\n<p>The most important concept. A Tree is a virtual representation of your filesystem. Schematics don&#8217;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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rule<\/h3>\n\n\n\n<p>A Rule is a function that takes a Tree and returns a new Tree. This is the heart of a schematic&#8217;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.).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">SchematicContext<\/h3>\n\n\n\n<p>&nbsp;An object that provides context and utilities for a Rule, such as logging and access to the schematic&#8217;s collection information.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">collection.json<\/h3>\n\n\n\n<p>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.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Templates<\/h3>\n\n\n\n<p>Schematics use special template files to generate dynamic output. You can use placeholders like &lt;%= variableName %&gt; that get replaced with the options provided by the user.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Step-by-Step Practical Example: ng generate feature<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Set Up the Schematics Project<\/h3>\n\n\n\n<p>First, you need the Schematics CLI.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm install -g @angular-devkit\/schematics-cli<\/code><\/pre>\n\n\n\n<p>Now, create a new blank schematics project.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>schematics blank --name=custom-schematics\ncd custom-schematics\nnpm install<\/code><\/pre>\n\n\n\n<p>Your project structure will look like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>custom-schematics\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 collection.json         # The manifest for your schematics\n\u2502   \u2514\u2500\u2500 custom-schematics\/      # A sample schematic\n\u2502       \u251c\u2500\u2500 index.js\n\u2502       \u2514\u2500\u2500 index_spec.js\n\u2514\u2500\u2500 package.json<\/code><\/pre>\n\n\n\n<p>Let&#8217;s rename the sample schematic to feature and convert the files to TypeScript.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>mv src\/custom-schematics src\/feature\nmv src\/feature\/index.js src\/feature\/index.ts\nmv src\/feature\/index_spec.js src\/feature\/index_spec.ts<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Define the Schematic in collection.json<\/h3>\n\n\n\n<p>This file is the entry point for the Angular CLI. Update src\/collection.json to describe our new feature schematic.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"$schema\": \"..\/node_modules\/@angular-devkit\/schematics\/collection-schema.json\",\n  \"schematics\": {\n    \"feature\": {\n      \"description\": \"Generates a new feature module with a component, service, and routing.\",\n      \"factory\": \".\/feature\/index#feature\",\n      \"schema\": \".\/feature\/schema.json\"\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li>&#8220;feature&#8221;: The name of our schematic (ng generate feature &#8230;).<\/li>\n\n\n\n<li>&#8220;factory&#8221;: Points to the main factory function (feature) inside index.ts.<\/li>\n\n\n\n<li>&#8220;schema&#8221;: Points to a JSON schema file where we&#8217;ll define the available command-line options.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Define the User Options in schema.json<\/h3>\n\n\n\n<p>Create a new file src\/feature\/schema.json. This file defines the parameters our schematic will accept, like &#8211;name.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"$schema\": \"http:\/\/json-schema.org\/schema\",\n  \"id\": \"FeatureSchematic\",\n  \"title\": \"Feature Schematic\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"name\": {\n      \"type\": \"string\",\n      \"description\": \"The name of the feature.\",\n      \"$default\": {\n        \"$source\": \"argv\",\n        \"index\": 0\n      },\n      \"x-prompt\": \"What is the name of the feature?\"\n    },\n    \"path\": {\n      \"type\": \"string\",\n      \"description\": \"The path to create the feature in.\",\n      \"default\": \"app\/features\"\n    }\n  },\n  \"required\": &#91;\n    \"name\"\n  ]\n}<\/code><\/pre>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>name<\/strong>: The feature&#8217;s name (e.g., &#8220;orders&#8221;, &#8220;profile&#8221;).\n<ul class=\"wp-block-list\">\n<li>$source: &#8220;argv&#8221;, index: 0 means the first argument passed in the command line will be used as the name (e.g., ng g feature my-feature).<\/li>\n\n\n\n<li>x-prompt provides a nice interactive prompt if the name isn&#8217;t supplied.<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><strong>path<\/strong>: Where to place the new feature folder. We default it to app\/features.<\/li>\n<\/ul>\n\n\n\n<p>We also need a schema.d.ts file to get TypeScript typings for our options. Create src\/feature\/schema.d.ts:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export interface Schema {\n  \/**\n   * The name of the feature.\n   *\/\n  name: string;\n  \/**\n   * The path to create the feature in.\n   *\/\n  path: string;\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: Create the Template Files<\/h3>\n\n\n\n<p>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., &#8220;my-feature&#8221;).<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>src\/feature\/files\/\n\u2514\u2500\u2500 __name@dasherize__\/\n    \u251c\u2500\u2500 __name@dasherize__.component.html\n    \u251c\u2500\u2500 __name@dasherize__.component.scss\n    \u251c\u2500\u2500 __name@dasherize__.component.spec.ts\n    \u251c\u2500\u2500 __name@dasherize__.component.ts\n    \u251c\u2500\u2500 __name@dasherize__.module.ts\n    \u251c\u2500\u2500 __name@dasherize__-routing.module.ts\n\u2514\u2500\u2500 __name@dasherize__.service.ts<\/code><\/pre>\n\n\n\n<p><strong>Example Template File:<\/strong><\/p>\n\n\n\n<p>Notice the use of &lt;%= &#8230; %> templating syntax. The classify and dasherize functions are helpers we&#8217;ll use in our logic.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { NgModule } from '@angular\/core';\nimport { CommonModule } from '@angular\/common';\n\nimport { &lt;%= classify(name) %>RoutingModule } from '.\/&lt;%= dasherize(name) %>-routing.module';\nimport { &lt;%= classify(name) %>Component } from '.\/&lt;%= dasherize(name) %>.component';\nimport { &lt;%= classify(name) %>Service } from '.\/&lt;%= dasherize(name) %>.service';\n\n@NgModule({\n  declarations: &#91;\n    &lt;%= classify(name) %>Component\n  ],\n  imports: &#91;\n    CommonModule,\n    &lt;%= classify(name) %>RoutingModule\n  ],\n  providers: &#91;\n    &lt;%= classify(name) %>Service\n  ]\n})\nexport class &lt;%= classify(name) %>Module { }<\/code><\/pre>\n\n\n\n<p><strong>Example Template File:<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { NgModule } from '@angular\/core';\nimport { RouterModule, Routes } from '@angular\/router';\nimport { &lt;%= classify(name) %>Component } from '.\/&lt;%= dasherize(name) %>.component';\n\nconst routes: Routes = &#91;{ path: '', component: &lt;%= classify(name) %>Component }];\n\n@NgModule({\n  imports: &#91;RouterModule.forChild(routes)],\n  exports: &#91;RouterModule]\n})\nexport class &lt;%= classify(name) %>RoutingModule { }<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Write the Main Schematic Logic in index.ts<\/h3>\n\n\n\n<p>This is where everything comes together. Replace the content of src\/feature\/index.ts with the following:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import {\n  Rule,\n  SchematicContext,\n  Tree,\n  apply,\n  url,\n  template,\n  move,\n  chain,\n  mergeWith,\n} from '@angular-devkit\/schematics';\nimport { strings } from '@angular-devkit\/core';\nimport { Schema } from '.\/schema';\nimport { addRouteDeclarationToNgModule } from '@schematics\/angular\/utility\/ast-utils';\nimport { InsertChange } from '@schematics\/angular\/utility\/change';\nimport * as ts from 'typescript';\n\n\/\/ Function to add the new route to app-routing.module.ts\nfunction addRouteToNgModule(options: Schema): Rule {\n  return (tree: Tree, _context: SchematicContext) => {\n    const appRoutingModulePath = 'src\/app\/app-routing.module.ts';\n    const featureName = options.name;\n    const featurePath = `${options.path}\/${strings.dasherize(featureName)}\/${strings.dasherize(featureName)}.module`;\n\n    const text = tree.read(appRoutingModulePath);\n    if (!text) {\n      _context.logger.error(`Could not find ${appRoutingModulePath}.`);\n      return tree;\n    }\n\n    const sourceText = text.toString('utf-8');\n    const sourceFile = ts.createSourceFile(\n      appRoutingModulePath,\n      sourceText,\n      ts.ScriptTarget.Latest,\n      true\n    );\n\n    const route = `{\n      path: '${strings.dasherize(featureName)}',\n      loadChildren: () => import('.\/features\/${strings.dasherize(featureName)}\/${strings.dasherize(featureName)}.module').then(m => m.${strings.classify(featureName)}Module)\n    }`;\n\n    \/\/ Use AST utils to find the routes array and insert the new route\n    const changes = addRouteDeclarationToNgModule(\n      sourceFile,\n      appRoutingModulePath,\n      route\n    );\n\n    const recorder = tree.beginUpdate(appRoutingModulePath);\n    for (const change of changes) {\n      if (change instanceof InsertChange) {\n        recorder.insertLeft(change.pos, change.toAdd);\n      }\n    }\n    tree.commitUpdate(recorder);\n\n    return tree;\n  };\n}\n\n\/\/ Main factory function\nexport function feature(_options: Schema): Rule {\n  return (_tree: Tree, _context: SchematicContext) => {\n\n    const templateSource = apply(url('.\/files'), &#91;\n      template({\n        ..._options,\n        ...strings, \/\/ Provides classify, dasherize, etc. to templates\n      }),\n      move(_options.path),\n    ]);\n\n    \/\/ Chain the two rules together\n    return chain(&#91;\n      mergeWith(templateSource),\n      addRouteToNgModule(_options)\n    ]);\n  };\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Explanation of the logic:<\/h2>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Function \/ Concept<\/strong><\/td><td><strong>Purpose \/ Explanation<\/strong><\/td><\/tr><tr><td><strong>function<\/strong><\/td><td>Main schematic factory where the rule chain is defined.<\/td><\/tr><tr><td><strong>url(&#8216;.\/files&#8217;)<\/strong><\/td><td>Reads template files from the .\/files directory. These are the files to scaffold.<\/td><\/tr><tr><td><strong>template({&#8230;})<\/strong><\/td><td>Replaces placeholders like &lt;%= %&gt; in the templates using user inputs and utility functions (classify, dasherize).<\/td><\/tr><tr><td><strong>move(&#8230;)<\/strong><\/td><td>Moves generated files to the path specified by the user (typically project structure).<\/td><\/tr><tr><td><strong>chain([&#8230;])<\/strong><\/td><td>Executes multiple Rules in order. Used here to generate files and then modify routing module.<\/td><\/tr><tr><td><strong>Rule (custom)<\/strong><\/td><td>A custom rule to edit an existing file (app-routing.module.ts).<\/td><\/tr><tr><td><strong>Reads app-routing.module.ts<\/strong><\/td><td>Accesses the routing module to inject new route information.<\/td><\/tr><tr><td><strong>Uses TypeScript AST<\/strong><\/td><td>Uses the Abstract Syntax Tree to safely parse and modify code (more accurate than regex).<\/td><\/tr><tr><td><strong>addRouteDeclarationToNgModule<\/strong><\/td><td>Utility from @schematics\/angular to help inject new routes into the NgModule.<\/td><\/tr><tr><td><strong>InsertChange<\/strong><\/td><td>A type of change object that represents text insertion. It&#8217;s applied to the file via the Tree.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Step 6: Build and Test the Schematic<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\">1. Build the Schematic<\/h4>\n\n\n\n<pre class=\"wp-block-code\"><code>npm run build<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">2. Link for Local Testing:<\/h4>\n\n\n\n<p>From inside your <strong>custom-schematics<\/strong> directory:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm link<\/code><\/pre>\n\n\n\n<p>Then, in your test Angular project:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd ..\/path\/to\/your\/angular-app\n# Link the global schematic to this project\nnpm link custom-schematics<\/code><\/pre>\n\n\n\n<h4 class=\"wp-block-heading\">3. Run the Schematic :<\/h4>\n\n\n\n<ol class=\"wp-block-list\"><\/ol>\n\n\n\n<p>Now you can run your custom schematic just like any other<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ng generate custom-schematics:feature orders<\/code><\/pre>\n\n\n\n<p>Or, if you set it as the default collection in angular.json :<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ng generate feature profile --path=app\/user<\/code><\/pre>\n\n\n\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Final Words<\/h2>\n\n\n\n<p>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<strong> <\/strong><a href=\"https:\/\/www.cmarix.com\/hire-angular-developers.html\">hire Angular developers<\/a> and maintain scalable, well-structured projects.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":2357,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[7,3],"tags":[],"class_list":["post-2354","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-angular","category-web"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/posts\/2354","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/comments?post=2354"}],"version-history":[{"count":3,"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/posts\/2354\/revisions"}],"predecessor-version":[{"id":2359,"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/posts\/2354\/revisions\/2359"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/media\/2357"}],"wp:attachment":[{"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/media?parent=2354"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/categories?post=2354"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.cmarix.com\/qanda\/wp-json\/wp\/v2\/tags?post=2354"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}