skip to content

How would you build an Angular CLI schematic so that `ng generate` scaffolds your team's feature folder with a routes file, a page component and a data service?

level: seniorimportance: should knowfreq 28%

answer

  1. a collection package in the workspace
  2. collection.json, schema.json, rule factory
  3. url('./files') plus applyTemplates
  4. move, then mergeWith, inside chain
  5. SchematicTestRunner and --dry-run

basics

~20 s

Write a schematics collection with a feature schematic whose rule renders url('./files') templates with applyTemplates, moves them into place and mergeWiths them into the tree; build it, list it in cli.schematicCollections, test it with SchematicTestRunner.

solid answer

~40 s

I would create a small schematics package in the workspace, say `@my-org/schematics`, whose `package.json` has a `schematics` field pointing at `collection.json`. That file lists a `feature` schematic with its `factory` and `schema.json`. The schema declares the options: `name` taken from the first positional argument, `project` defaulted from the current project, maybe `withStore`, with `x-prompt` for anything required. The factory reads the project's source root from the workspace. It then builds a template source with `apply(url('./files'), [applyTemplates({ ...strings, ...options }), move(targetPath)])` and returns `chain([mergeWith(source), ...extra rules])`, for example a rule that registers the lazy route. Template file names like `__name@dasherize__.routes.ts.template` become real names. The package is compiled to JS and added first to `cli.schematicCollections`, so `ng g feature orders` works. Unit tests use `SchematicTestRunner`, and developers preview with `--dry-run`.

code

json · 12 lines
json
{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "extends": "@schematics/angular",
  "schematics": {
    "feature": {
      "description": "Scaffold a lazy feature folder.",
      "factory": "./feature/index#feature",
      "schema": "./feature/schema.json",
      "aliases": ["f"]
    }
  }
}

go deeper

for a junior

Know that teams can add their own generators to ng generate by writing schematics.

for a middle

Describe collection.json, schema.json and a rule factory, and how template files become real files.

for a senior

Build the full pipeline: templated source with apply, move and mergeWith, chained edits and externalSchematic, registration via schematicCollections, and SchematicTestRunner tests.

for a principal

Decide what a shared generator should own versus leave to the built-ins, how it is versioned and published across repos, and who maintains it.

## What the team actually needs The goal is that `ng g feature orders` produces the same layout every time: `features/orders/orders.routes.ts`, an `orders-page` component and an `orders-data` service, named and wired the way the team agreed. A shell script could copy files. A **schematic** does it with option validation, prompts, dry runs, all-or-nothing writes, and integration with `ng generate`. It can also chain the built-in component and service schematics so their v22 defaults stay current. ## 1. The collection package A collection is an npm-style package whose `package.json` has a `schematics` field pointing at `collection.json`. In a monorepo it can live in the workspace and be built with the rest. `collection.json` maps schematic names to their parts: - `factory`: the compiled module and exported function, for example `./feature/index#feature`; - `schema`: the JSON schema describing options; - `description`, plus optional `aliases`, `hidden` or `private`. A collection can also `extends` another collection, such as `@schematics/angular`, so the team's collection answers every built-in name too. ## 2. The options schema `schema.json` is what turns into CLI flags: - `name` with `"$default": { "$source": "argv", "index": 0 }`, so `ng g feature orders` fills it from the first positional argument; - `project` with `"$default": { "$source": "projectName" }`, so it defaults to the project you are in; - booleans such as `withStore`, and `x-prompt` text for anything you want asked interactively. The CLI validates input against the schema before the factory runs, so the rule needs no manual type checks. ## 3. The rule factory The factory receives the options and returns a `Rule`. The built-in `component` schematic in `@schematics/angular` follows exactly this pattern: 1. Read the workspace (`readWorkspace` from `@schematics/angular/utility`) to find the project's `sourceRoot`, and compute `src/app/features/<name>`. 2. Build a **template source**: `apply(url('./files'), [...])`. Inside it, `applyTemplates({ ...strings, ...options })` processes every `*.template` file. File-name tokens like `__name@dasherize__` become `orders`, contents like `<%= classify(name) %>Page` become `OrdersPage`, and the `.template` suffix is removed. `move(targetPath)` relocates the files, and `filter(...)` drops files the options exclude. 3. Return `chain([...])`: `mergeWith(templateSource)` merges the generated files into the tree, followed by rules that edit existing code. Two examples are adding the lazy route to `app.routes.ts` with an `UpdateRecorder`, or `externalSchematic('@schematics/angular', 'component', {...})` to reuse the official component generator. The `strings` helpers (`dasherize`, `classify`, `camelize`) are exported by `@angular-devkit/schematics`, and `@schematics/angular/utility` adds workspace readers and rules such as `addRootProvider` and `addDependency`. A template file is plain text with EJS-style tags. For example, `files/__name@dasherize__.routes.ts.template` might contain: ```ts import { Routes } from '@angular/router'; export const <%= camelize(name) %>Routes: Routes = [ { path: '', loadComponent: () => import('./<%= dasherize(name) %>-page').then((m) => m.<%= classify(name) %>Page) }, ]; ``` With `name` set to `orders`, it becomes `orders.routes.ts` exporting `ordersRoutes`, which lazy-loads `OrdersPage`. ## 4. Wiring it into `ng generate` - Compile the TypeScript to JS, and copy `schema.json` and the `files/` folder next to it, because `factory` points at compiled code. - Install or link the package, then set `cli.schematicCollections` to `["@my-org/schematics", "@schematics/angular"]`. `ng g feature orders` now resolves `feature` from the team collection, while `component` and `service` still come from `@schematics/angular` unless the team shadows them. - Optionally store team defaults such as `"@my-org/schematics:feature": { "withStore": true }` in the `schematics` block of `angular.json`. ## 5. Testing | Level | Tool | Checks | |---|---|---| | Unit | `SchematicTestRunner` from `@angular-devkit/schematics/testing` | `runSchematic('feature', opts, tree)` returns a `UnitTestTree`; assert `files` and `readContent()` | | Integration | `ng g feature demo --dry-run` in the real workspace | Paths and edits look right, nothing written | | Smoke | Generate into a scratch branch, then build and test | Output compiles and passes lint | For unit tests, seed the input tree by first running `@schematics/angular`'s `workspace` and `application` schematics through `runExternalSchematic`, so the feature schematic sees a realistic project. ## Pitfalls - Calling `tree.create()` for files that may exist, instead of checking `tree.exists()`. - Forgetting to ship `files/` and `schema.json` in the build output, which makes generation fail at runtime. - Hard-coding `src/app` instead of reading the project's `sourceRoot`, which breaks libraries and multi-project workspaces. - Copying the built-in component template, and so freezing today's defaults, instead of chaining `externalSchematic`.

  • Why chain `externalSchematic('@schematics/angular', 'component', ...)` instead of shipping your own component template?
    The official generator tracks framework defaults: the 2025 file names, `OnPush` by omission in v22, standalone. It also handles the NgModule case. A copied template freezes today's output and silently drifts after every `ng update`, while chaining keeps the team's feature layout and delegates the component itself.
  • How do you unit-test the feature schematic without a real workspace on disk?
    Create a `SchematicTestRunner` for your collection. Build an input tree by running `@schematics/angular`'s `workspace` and `application` schematics with `runExternalSchematic`. Then call `runSchematic('feature', { name: 'orders', project: 'app' }, tree)` and assert on the returned `UnitTestTree`'s `files` and `readContent()` for each generated path.
  • Where should the step that adds the lazy route to `app.routes.ts` live?
    In a separate rule in the same `chain`, after the merge. It reads `app.routes.ts` from the tree, finds the insertion point by parsing rather than by string position where possible, and inserts through `beginUpdate`/`commitUpdate`. It should check first whether the route already exists, so re-running is harmless.

saying these in an interview costs you the question

  • Writes the feature scaffolder as a shell script that copies files with cp
  • Points collection.json factory at the .ts source and never compiles it
  • Hard-codes src/app instead of reading the project's sourceRoot
  • Copies the built-in component template and so freezes old defaults
  • Tests the schematic only by running it for real in the main repo