This guide shows how to create an Angular CLI workspace where our “features” live in two libraries (e.g. topology and bri) and a host application composes them. This is a good approach when we want modular boundaries but still ship a single app artifact (no runtime federation, no extra ports).

0) Prerequisites

  • Node.js LTS
  • Angular CLI (locally or via npx)

1) Create a new workspace + host application

Option A (classic): create an app inside the workspace:

npx @angular/cli@latest new itop-topology-ui --create-application=true
cd itop-topology-ui

This produces:

  • projects/itop-topology-ui/ (app)
  • angular.jsontsconfig.*.json, etc.

2) Generate 2 feature libraries

We’ll create:

  • @itop/topology (feature lib)
  • @itop/bri (feature lib)
ng generate library topology --prefix itop
ng generate library bri --prefix itop

We now have:

  • projects/topology/
  • projects/bri/

Each library has its own ng-package.json, src/public-api.ts, and tsconfig.lib.json.

--prefix defines the component selector prefix for artifacts generated inside that library (components, directives). It mainly affects HTML selectors and helps avoid naming collisions.

If you run (inside the lib):

ng g component topology-home --project topology
Angular will generate a selector like "itop-topology-home" (because prefix = itop)
So you would use it here:
<itop-topology-home></itop-topology-home>

3) Design the libraries as feature modules

A common pattern is: each feature library exports a module + routes.

3.1 Topology library: module + routing

Create files:

projects/topology/src/lib/topology.component.ts

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

@Component({
  selector: 'itop-topology',
  template: `
    <h2>Topology</h2>
    <p>This is the Topology feature library.</p>
  `,
})
export class TopologyComponent {}

projects/topology/src/lib/topology.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { TopologyComponent } from './topology.component';

@NgModule({
  declarations: [TopologyComponent],
  imports: [
    CommonModule,
    RouterModule.forChild([
      { path: '', component: TopologyComponent },
    ]),
  ],
})
export class TopologyModule {}

Export it from the library public API:

projects/topology/src/public-api.ts

export * from './lib/topology.module';
3.2 BRI library: module + routing

projects/bri/src/lib/bri.component.ts

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

@Component({
  selector: 'itop-bri',
  template: `
    <h2>BRI</h2>
    <p>This is the BRI feature library.</p>
  `,
})
export class BriComponent {}

projects/bri/src/lib/bri.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { BriComponent } from './bri.component';

@NgModule({
  declarations: [BriComponent],
  imports: [
    CommonModule,
    RouterModule.forChild([
      { path: '', component: BriComponent },
    ]),
  ],
})
export class BriModule {}

projects/bri/src/public-api.ts

export * from './lib/bri.module';

4) Wire the host app routes to the libraries (lazy-loaded)

In the host app, keep the URLs stable and delegate ownership to the feature libs.

projects/itop-topology-ui/src/app/app-routing.module.ts (NgModule-based routing)

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'route-network',
    loadChildren: () =>
      import('topology').then((m) => m.TopologyModule),
  },
  {
    path: 'bri',
    loadChildren: () =>
      import('bri').then((m) => m.BriModule),
  },
  { path: '', redirectTo: 'route-network', pathMatch: 'full' },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Key point: the import paths ('topology', 'bri') come from TypeScript path mapping (next step).


5) TypeScript configuration (tsconfig path mapping)

To import libraries by a clean name (instead of deep relative paths), configure paths in the workspace base tsconfig.

5.1 Update tsconfig.base.json (or tsconfig.json)

Angular CLI typically uses tsconfig.base.json. If your repo only has tsconfig.json, apply the same idea there.

tsconfig.base.json

{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "topology": ["dist/topology"],
      "bri": ["dist/bri"]
    }
  }
}

Why dist/...? Because Angular libraries are compiled/built into dist/<libName>, and those outputs contain the generated typings and entry points. (This is a good choice if you plan to publish the libraries later; otherwise, you can map the imports directly to the source via projects/... paths instead.).

Use projects/.../public-api.ts when:
  • You want fast local dev (no “build libs first” friction).
  • Libraries are part of the same repo and not independently published.
  • You want the host app to compile against sources.
Use dist/... when:
  • You want to treat libs like compiled packages (closer to what consumers get).
  • Your pipeline explicitly builds libs first (CI/build orchestrations).
  • You want to ensure nothing outside the packaged output is accidentally consumed.
5.2 Dev note: build libs (when needed)

In many setups, ng serve will compile what it needs, but if you see resolution errors, build the libs once:

ng build topology
ng build bri
ng serve itop-topology-ui

6) angular.json essentials (projects, build/test targets)

When we run ng generate library ..., Angular CLI updates angular.json automatically.

We should verify we have entries like:

  • projects.itop-topology-ui.architect.build
  • projects.itop-topology-ui.architect.test
  • projects.topology.architect.build
  • projects.topology.architect.test
  • projects.bri.architect.build
  • projects.bri.architect.test
{
  "projects": {
    "itop-topology-ui": {
      "projectType": "application",
      "architect": {
        "build": { "...": "..." },
        "test": { "...": "..." }
      }
    },
    "topology": {
      "projectType": "library",
      "architect": {
        "build": { "...": "..." },
        "test": { "...": "..." }
      }
    },
    "bri": {
      "projectType": "library",
      "architect": {
        "build": { "...": "..." },
        "test": { "...": "..." }
      }
    }
  }
}

If your CI requires a dedicated “ci” configuration, you can add test configurations under each project:

"test": {
  "options": { "...": "..." },
  "configurations": {
    "ci": {
      "watch": false,
      "codeCoverage": true
    }
  }
}

PS: (Exact keys depend on your Angular/Karma setup, but the shape above is the common approach.)


7) Testing & coverage per project

We can run tests per project:

ng test itop-topology-ui --code-coverage
ng test topology --code-coverage
ng test bri --code-coverage

In CI we typically run them all (and optionally merge coverage afterwards).


8) Recommended workspace conventions (to keep boundaries clean)

  • Never import from the host app into libraries
    e.g. avoid import ... from 'projects/itop-topology-ui/src/app/...'
  • Put cross-cutting code in dedicated libs:
    • shared-uishared-modelsauthpermissions, etc.
  • Expose only what’s needed via each lib’s public-api.ts.

9) Common gotchas (and how to avoid them)

  1. Path mapping points to dist/<lib>
    If imports like import('topology') fail, build the lib once or ensure the paths are correct.
  2. Deep imports
    Don’t import topology/src/lib/... from the app; export from public-api.ts.
  3. Routing duplication
    Keep feature routes inside the library module (via RouterModule.forChild) and only mount them in the host.

By Shabazz

Software Engineer, MCSD, Web developer & Angular specialist , BizDevOps

Leave a Reply

Your email address will not be published. Required fields are marked *