This is Part 3 of a 6-part series on Angular Schematics.
- ✅ Part 1 – Understanding Angular Schematics: Architecture & Core Concepts
- ✅ Part 2 – Creating Custom Generators with ng generate
- Part 3 – Building Installation Schematics with ng add ← You are here
- 🔜 Part 4 – Writing Migration Schematics with ng update
- 🔜 Part 5 – Testing Schematics with Angular DevKit
- 🔜 Part 6 – Advanced Patterns, Publishing & Nx Integration
🤖 A note on this article: I used Claude to help reformat and structure the content to make it clearer and more presentable for publication.
References
- Angular DevKit Schematics – Full README – Complete reference for
@angular-devkit/schematics - Angular CLI – Adding Libraries – Official guide for ng add schematic support
In Part 2 we built a custom ng generate schematic that scaffolds a component with your organisation’s selector prefix. We understand the workspace setup, collection.json, schema.json, factory functions, and file templates.
Part 3 is about a different problem entirely. Not generating code inside a project that already has your library – but automating what happens the very first time a developer installs it.
When a developer runs:
ng add @acme/ui
They should not have to read a 12-step setup guide. The library should configure itself – install its peer dependencies, update angular.json, register its module, add any required styles, and leave the workspace in a working state. That is what an ng add schematic does.
By the end of this article you will have built a complete ng add schematic that handles the full installation lifecycle of a library.
Table of Contents
- What Happens When You Run ng add
- How ng add Differs from ng generate
- Registering the ng-add Entry Point
- The Installation Schematic – Full Walkthrough
- Step 1 – Adding Dependencies to package.json
- Step 2 – Scheduling npm install
- Step 3 – Updating angular.json
- Step 4 – Adding Global Styles
- Putting It All Together
- Running and Testing Locally
- Why This Matters – The Developer Experience Argument
- ng add vs ng generate vs ng update – The Complete Picture
- Summary & What’s Next
What Happens When You Run ng add
ng add is a first-class Angular CLI command designed for one specific job: installing and configuring a library from scratch. When you run it, the CLI executes the following sequence:
ng add @acme/ui
│
▼
1. npm install @acme/ui (if not already installed)
│
▼
2. Read package.json of @acme/ui
→ find "schematics": "./schematics/collection.json"
│
▼
3. Read collection.json
→ find the "ng-add" schematic entry
│
▼
4. Execute the ng-add factory function
→ Tree mutations staged
→ Tasks scheduled (e.g. NodePackageInstallTask)
│
▼
5. Commit staged Tree to disk
6. Run scheduled tasks (npm install for peer deps)
The key distinction from ng generate is step 1 – the CLI installs the package itself before running the schematic. This means by the time your factory function runs, the package is already in node_modules and its own package.json is available. You have access to version information, peer dependency declarations, and any assets the package ships.

How ng add Differs from ng generate
Both use the same underlying DevKit primitives – Tree, Rule, chain, applyTemplates. The differences are in purpose, trigger, and what the CLI does around the schematic:
ng generate | ng add | |
|---|---|---|
| Purpose | Scaffold code in an existing project | Install and configure a library for the first time |
| Trigger | Manual, on demand, repeatable | Once per library installation |
| Package install | No – package must already be installed | Yes – CLI installs the package first |
| Entry in collection.json | Named schematic e.g. "component" | Reserved name "ng-add" |
| Idempotency expectation | Usually idempotent | Should guard against running twice |
| Typical operations | Create files, modify source | Update package.json, angular.json, AppModule, styles |
The reserved name "ng-add" is important – the CLI looks for exactly this key in collection.json. Any other name will not be found by ng add.
Registering the ng-add Entry Point
In your library’s collection.json, add the ng-add entry alongside any existing schematics:
{
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
"schematics": {
"ng-add": {
"description": "Installs and configures @acme/ui in the workspace",
"factory": "./ng-add/index#ngAdd",
"schema": "./ng-add/schema.json"
}
}
}
And in your library’s package.json, the „schematics” field must point to this collection:
{
"name": "@acme/ui",
"version": "1.0.0",
"schematics": "./schematics/collection.json"
}
The project structure for the ng-add schematic sits inside your library’s schematics folder:
@acme/ui/
├── package.json
└── schematics/
├── collection.json
└── ng-add/
├── index.ts ← factory function
├── schema.json ← options
└── schema.ts ← TypeScript interface
The Installation Schematic – Full Walkthrough
Let’s define what our ng add schematic needs to do when a developer installs @acme/ui:
- Add any required peer dependencies to
package.json - Trigger
npm installto install them - Add the library’s stylesheet path to
angular.json - Add the library’s base CSS import to the global
styles.scss
Each of these is a separate Rule. We’ll build each one, then compose them.
First, define the schema. Create schematics/ng-add/schema.json:
"$schema": "http://json-schema.org/schema",
"$id": "NgAddSchema",
"title": "ng-add schematic for @acme/ui",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The name of the Angular project to configure",
"$default": { "$source": "projectName" }
},
"skipStyles": {
"type": "boolean",
"description": "Skip adding the global stylesheet",
"default": false
}
}
}
And schematics/ng-add/schema.ts:
export interface NgAddSchema {
project: string;
skipStyles: boolean;
}
Step 1 – Adding Dependencies to package.json
The first rule adds any peer dependencies the library requires to the consumer’s package.json. We read the file, add the entries to the dependencies object, and write it back.
import { Rule, Tree, SchematicsException } from '@angular-devkit/schematics';
function addDependencies(): Rule {
return (tree: Tree) => {
const pkgPath = 'package.json';
const buffer = tree.read(pkgPath);
if (!buffer) {
throw new SchematicsException('Could not find package.json in workspace root.');
}
const pkg = JSON.parse(buffer.toString('utf-8'));
// Add peer dependencies your library requires
pkg.dependencies = pkg.dependencies || {};
pkg.dependencies['@acme/ui'] = '^1.0.0';
pkg.dependencies['@acme/ui-icons'] = '^1.0.0'; // example peer dep
tree.overwrite(pkgPath, JSON.stringify(pkg, null, 2));
return tree;
};
}
💡 Idempotency: Always check whether the dependency already exists before writing. If you are running the schematic again on a workspace that already has the package, you should not downgrade or overwrite a version the developer has already pinned to.
A more robust version guards against overwriting existing entries:
if (!pkg.dependencies['@acme/ui-icons']) {
pkg.dependencies['@acme/ui-icons'] = '^1.0.0';
}
Step 2 – Scheduling npm install
Writing entries to package.json does not install them. For that, you schedule a NodePackageInstallTask via the SchematicContext. Tasks run after all Rules have executed and the Tree has been committed to disk – so by the time npm install fires, your updated package.json is already on disk.
import {
Rule, Tree, SchematicContext
} from '@angular-devkit/schematics';
import {
NodePackageInstallTask
} from '@angular-devkit/schematics/tasks';
function installDependencies(): Rule {
return (_tree: Tree, context: SchematicContext) => {
context.addTask(new NodePackageInstallTask());
context.logger.info('Scheduling npm install...');
};
}
The NodePackageInstallTask runs npm install (or yarn install / pnpm install – the CLI detects the package manager in use). You do not need to specify what to install – it simply runs the package manager against the current package.json, picking up whatever your previous Rule wrote.
Step 3 – Updating angular.json
Many libraries ship assets – fonts, icon sets, pre-built CSS – that need to be registered in the styles or assets arrays of angular.json. Reading and writing angular.json follows the same JSON parse-mutate-reserialise pattern, but requires navigating to the right project node first.
import { Rule, Tree, SchematicsException } from '@angular-devkit/schematics';
import { NgAddSchema } from './schema';
function addStylesToAngularJson(options: NgAddSchema): Rule {
return (tree: Tree) => {
const angularJsonPath = 'angular.json';
const buffer = tree.read(angularJsonPath);
if (!buffer) {
throw new SchematicsException('Could not find angular.json.');
}
const angularJson = JSON.parse(buffer.toString('utf-8'));
// Resolve the target project - falls back to defaultProject
const projectName =
options.project ||
angularJson.defaultProject;
const project = angularJson.projects[projectName];
if (!project) {
throw new SchematicsException(
`Project "${projectName}" not found in angular.json.`
);
}
const buildOptions = project.architect?.build?.options;
if (!buildOptions) {
throw new SchematicsException(
`Could not find build options for project "${projectName}".`
);
}
// Add the library's pre-built stylesheet
const styleEntry = 'node_modules/@acme/ui/styles/acme-ui.css';
buildOptions.styles = buildOptions.styles || [];
if (!buildOptions.styles.includes(styleEntry)) {
buildOptions.styles.unshift(styleEntry); // prepend so it loads before app styles
tree.overwrite(angularJsonPath, JSON.stringify(angularJson, null, 2));
}
return tree;
};
}
The unshift rather than push is intentional – library base styles should load before application styles so that application styles can safely override them.
Step 4 – Adding Global Styles
Some libraries require a base import in the application’s global stylesheet – a CSS custom property definition, a font face declaration, or a reset. Rather than using angular.json’s styles array for this, some setups prefer an explicit @import in styles.scss.
import { Rule, Tree } from '@angular-devkit/schematics';
import { NgAddSchema } from './schema';
function addGlobalStyleImport(options: NgAddSchema): Rule {
return (tree: Tree) => {
if (options.skipStyles) return tree;
// Support both .scss and .css global stylesheets
const stylePaths = [
'src/styles.scss',
'src/styles.css',
'src/styles.sass',
];
const stylePath = stylePaths.find(p => tree.exists(p));
if (!stylePath) return tree; // no global stylesheet found - skip silently
const content = tree.read(stylePath)!.toString('utf-8');
const importLine = `@import '@acme/ui/styles/tokens';\n`;
if (content.includes(importLine)) return tree; // idempotent guard
tree.overwrite(stylePath, importLine + content); // prepend
return tree;
};
}
The skipStyles option from schema.json gives consumers an escape hatch – useful when a workspace has a custom styling pipeline that handles this differently.

Putting It All Together
The factory function composes all four Rules into a single chain:
import {
Rule, SchematicContext, Tree, chain
} from '@angular-devkit/schematics';
import { NgAddSchema } from './schema';
export function ngAdd(options: NgAddSchema): Rule {
return (tree: Tree, context: SchematicContext) => {
context.logger.info('Setting up @acme/ui...');
return chain([
addDependencies(),
installDependencies(),
addStylesToAngularJson(options),
addGlobalStyleImport(options),
])(tree, context);
};
}
The order matters here. addDependencies() must run before installDependencies() so that package.json is updated before npm install reads it. Everything else can run in any order, but grouping file operations before the install task is clean practice.
When a developer runs ng add @acme/ui, the terminal output looks like this:
✔ Package successfully installed.
UPDATE package.json (1842 bytes)
UPDATE angular.json (4103 bytes)
UPDATE src/styles.scss (112 bytes)
✔ Packages installed successfully.
Every file that was touched is listed. The developer sees exactly what changed before they commit.
Running and Testing Locally
Because ng add installs the package first, local testing requires a slightly different approach than npm link:
# Pack the library as a tarball
cd your-library
npm run build
npm pack
# Produces: acme-ui-1.0.0.tgz
# In your test Angular workspace
ng add ./path/to/acme-ui-1.0.0.tgz
# Or install from a local path and run ng add separately
npm install ../your-library
ng add @acme/ui
# Preview without committing
ng add @acme/ui --dry-run
The --dry-run flag is particularly valuable for ng add schematics because the changes are spread across multiple files. Reviewing the full diff before committing gives the developer confidence in what the schematic is doing to their workspace.
Why This Matters – The Developer Experience Argument
A library without an ng add schematic asks its consumers to do three things: read documentation, understand the configuration, and execute a series of manual steps correctly. Each of those steps is an opportunity for error, and errors in setup produce confusing runtime failures that are hard to diagnose.
A library with a well-written ng add schematic asks its consumers to do one thing: run one command. The rest is guaranteed.
This is not a minor convenience – it is the difference between a library that gets adopted and one that doesn’t. Developers evaluate libraries partly on how painful the setup is. If the first experience of your library is a frictionless ng add that leaves the workspace correctly configured and immediately working, that is a strong signal that the library is well-maintained and the team behind it cares about developer experience.
The Angular ecosystem has established ng add as the expected installation path for any serious library. Angular Material, NgRx, Angular CDK, Transloco, TailwindCSS, PrimeNG – all of them ship ng add schematics. Consumer developers now expect it. If your library doesn’t have one, it stands out – for the wrong reason.
Summary & What’s Next
We built a complete ng add schematic that handles the full installation lifecycle of a library: adding dependencies to package.json, scheduling npm install, updating angular.json with the library’s stylesheet, and adding a global style import – all from a single ng add command.
The key concepts to take forward:
- The reserved
"ng-add"key incollection.jsonis what makesng addwork – no other name will be picked up by the CLI. - The CLI installs the package before running the schematic – your factory function runs with the package already in
node_modules. NodePackageInstallTaskruns after all Rules commit – always schedule it last so it picks up your updatedpackage.json.- Every file operation should be guarded with an existence check and an idempotency check –
ng addshould be safe to run on a workspace that was already configured. - The
--dry-runflag is your best friend during development – always test with it first.
In Part 4 we turn to the third and final schematic type: ng update migrations. We’ll cover migrations.json, version-gating, and using the TypeScript Compiler API to safely rewrite existing source code – the mechanism that makes breaking library changes manageable at scale.
Series Roadmap
| Part | Topic | Status |
|---|---|---|
| Part 1 | Understanding Angular Schematics – Architecture & Core Concepts | ✅ Published |
| Part 2 | Creating Custom Generators with ng generate | ✅ Published |
| Part 3 | Building Installation Schematics with ng add | ✅ You are here |
| Part 4 | Writing Migration Schematics with ng update | 🔜 Coming Soon |
| Part 5 | Testing Schematics with Angular DevKit | 🔜 Coming Soon |
| Part 6 | Advanced Patterns, Publishing & Nx Integration | 🔜 Coming Soon |
Built with Angular v21 · @angular-devkit/schematics · @angular-devkit/schematics/tasks · @angular-devkit/core