{"id":4235,"date":"2026-02-24T15:23:48","date_gmt":"2026-02-24T14:23:48","guid":{"rendered":"https:\/\/nguenkam.com\/blog\/?p=4235"},"modified":"2026-02-24T16:01:31","modified_gmt":"2026-02-24T15:01:31","slug":"building-an-angular-workspace-with-2-feature-libraries-step-by-step","status":"publish","type":"post","link":"https:\/\/nguenkam.com\/blog\/index.php\/2026\/02\/24\/building-an-angular-workspace-with-2-feature-libraries-step-by-step\/","title":{"rendered":"Building an Angular Workspace with 2 Feature Libraries (Step-by-step)"},"content":{"rendered":"\n<p>This guide shows how to create an <strong>Angular CLI workspace<\/strong> where our \u201cfeatures\u201d live in <strong>two libraries<\/strong> (e.g. <code>topology<\/code> and <code>bri<\/code>) and a <strong>host application<\/strong> composes them. This is a good approach when we want modular boundaries but still ship <strong>a single app artifact<\/strong> (no runtime federation, no extra ports).<\/p>\n\n\n\n<h4>0) Prerequisites<\/h4>\n\n\n\n<ul><li>Node.js LTS<\/li><li>Angular CLI (locally or via&nbsp;<code>npx<\/code>)<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>1) Create a new workspace + host application<\/h4>\n\n\n\n<p>Option A (classic): create an app inside the workspace:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npx @angular\/cli@latest new itop-topology-ui --create-application=true\ncd itop-topology-ui\n<\/code><\/pre>\n\n\n\n<p>This produces:<\/p>\n\n\n\n<ul><li><code>projects\/itop-topology-ui\/<\/code>&nbsp;(app)<\/li><li><code>angular.json<\/code>,&nbsp;<code>tsconfig.*.json<\/code>, etc.<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>2) Generate 2 feature libraries<\/h4>\n\n\n\n<p>We\u2019ll create:<\/p>\n\n\n\n<ul><li><code>@itop\/topology<\/code>&nbsp;(feature lib)<\/li><li><code>@itop\/bri<\/code>&nbsp;(feature lib)<\/li><\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>ng generate library topology --prefix itop\nng generate library bri --prefix itop\n<\/code><\/pre>\n\n\n\n<p>We now have:<\/p>\n\n\n\n<ul><li><code>projects\/topology\/<\/code><\/li><li><code>projects\/bri\/<\/code><\/li><\/ul>\n\n\n\n<p>Each library has its own <code>ng-package.json<\/code>, <code>src\/public-api.ts<\/code>, and <code>tsconfig.lib.json<\/code>.<\/p>\n\n\n\n<p><\/p>\n\n\n\n<p><code>--prefix<\/code> defines the <strong>component selector prefix<\/strong> for artifacts generated <em>inside that library<\/em> (components, directives). It mainly affects <strong>HTML selectors<\/strong> and helps avoid naming collisions.<\/p>\n\n\n\n<p>If you run (inside the lib):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ng g component topology-home --project topology\r<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-preformatted\">Angular will generate a selector like \"<code>itop-topology-home<\/code>\"\u00a0(because prefix =\u00a0<code>itop<\/code>)\nSo you would use it here:<\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>&lt;itop-topology-home>&lt;\/itop-topology-home><\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>3) Design the libraries as&nbsp;<em>feature modules<\/em><\/h4>\n\n\n\n<p>A common pattern is: <strong>each feature library exports a module + routes<\/strong>.<\/p>\n\n\n\n<h5><span class=\"has-inline-color has-vivid-cyan-blue-color\">3.1 Topology library: module + routing<\/span><\/h5>\n\n\n\n<p>Create files:  <\/p>\n\n\n\n<p><strong><code>projects\/topology\/src\/lib\/topology.component.ts<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { Component } from '@angular\/core';\n\n@Component({\n  selector: 'itop-topology',\n  template: `\n    &lt;h2&gt;Topology&lt;\/h2&gt;\n    &lt;p&gt;This is the Topology feature library.&lt;\/p&gt;\n  `,\n})\nexport class TopologyComponent {}\n<\/code><\/pre>\n\n\n\n<p><strong><code>projects\/topology\/src\/lib\/topology.module.ts<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { NgModule } from '@angular\/core';\nimport { CommonModule } from '@angular\/common';\nimport { RouterModule } from '@angular\/router';\nimport { TopologyComponent } from '.\/topology.component';\n\n@NgModule({\n  declarations: &#91;TopologyComponent],\n  imports: &#91;\n    CommonModule,\n    RouterModule.forChild(&#91;\n      { path: '', component: TopologyComponent },\n    ]),\n  ],\n})\nexport class TopologyModule {}<\/code><\/pre>\n\n\n\n<p>Export it from the library public API:<\/p>\n\n\n\n<p><strong><code>projects\/topology\/src\/public-api.ts<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export * from '.\/lib\/topology.module';<\/code><\/pre>\n\n\n\n<h5><span class=\"has-inline-color has-vivid-cyan-blue-color\">3.2 BRI library: module + routing<\/span><\/h5>\n\n\n\n<p><strong><code>projects\/bri\/src\/lib\/bri.component.ts<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { Component } from '@angular\/core';\n\n@Component({\n  selector: 'itop-bri',\n  template: `\n    &lt;h2>BRI&lt;\/h2>\n    &lt;p>This is the BRI feature library.&lt;\/p>\n  `,\n})\nexport class BriComponent {}<\/code><\/pre>\n\n\n\n<p><strong><code>projects\/bri\/src\/lib\/bri.module.ts<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { NgModule } from '@angular\/core';\nimport { CommonModule } from '@angular\/common';\nimport { RouterModule } from '@angular\/router';\nimport { BriComponent } from '.\/bri.component';\n\n@NgModule({\n  declarations: &#91;BriComponent],\n  imports: &#91;\n    CommonModule,\n    RouterModule.forChild(&#91;\n      { path: '', component: BriComponent },\n    ]),\n  ],\n})\nexport class BriModule {}<\/code><\/pre>\n\n\n\n<p><strong><code>projects\/bri\/src\/public-api.ts<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export * from '.\/lib\/bri.module';<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>4) Wire the host app routes to the libraries (lazy-loaded)<\/h4>\n\n\n\n<p>In the host app, keep the URLs stable and delegate ownership to the feature libs.<\/p>\n\n\n\n<p><strong><code>projects\/itop-topology-ui\/src\/app\/app-routing.module.ts<\/code><\/strong> (NgModule-based routing)<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { NgModule } from '@angular\/core';\nimport { RouterModule, Routes } from '@angular\/router';\n\nconst routes: Routes = &#91;\n  {\n    path: 'route-network',\n    loadChildren: () =>\n      import('topology').then((m) => m.TopologyModule),\n  },\n  {\n    path: 'bri',\n    loadChildren: () =>\n      import('bri').then((m) => m.BriModule),\n  },\n  { path: '', redirectTo: 'route-network', pathMatch: 'full' },\n];\n\n@NgModule({\n  imports: &#91;RouterModule.forRoot(routes)],\n  exports: &#91;RouterModule],\n})\nexport class AppRoutingModule {}<\/code><\/pre>\n\n\n\n<p>Key point: the import paths (<code>'topology'<\/code>, <code>'bri'<\/code>) come from TypeScript path mapping (next step).<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>5) TypeScript configuration (tsconfig path mapping)<\/h4>\n\n\n\n<p>To import libraries by a clean name (instead of deep relative paths), configure <code>paths<\/code> in the workspace base tsconfig.<\/p>\n\n\n\n<h5><span class=\"has-inline-color has-vivid-cyan-blue-color\">5.1 Update&nbsp;<code>tsconfig.base.json<\/code>&nbsp;(or&nbsp;<code>tsconfig.json<\/code>)<\/span><\/h5>\n\n\n\n<p>Angular CLI typically uses <strong><code>tsconfig.base.json<\/code><\/strong>. If your repo only has <code>tsconfig.json<\/code>, apply the same idea there.<\/p>\n\n\n\n<p><strong><code>tsconfig.base.json<\/code><\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"compileOnSave\": false,\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"topology\": &#91;\"dist\/topology\"],\n      \"bri\": &#91;\"dist\/bri\"]\n    }\n  }\n}<\/code><\/pre>\n\n\n\n<p>Why <code>dist\/...<\/code>? Because Angular libraries are compiled\/built into <code>dist\/&lt;libName><\/code>, and those outputs contain the generated typings and entry points.<strong> <span class=\"has-inline-color has-vivid-red-color\">(This is a good choice if you plan to publish the libraries later; otherwise, you can map the imports directly to the source via <code>projects\/...<\/code> paths instead.)<\/span><\/strong>.<\/p>\n\n\n\n<h5>Use&nbsp;<code>projects\/...\/public-api.ts<\/code>&nbsp;when:<\/h5>\n\n\n\n<ul><li>You want\u00a0<strong>fast local dev<\/strong>\u00a0(no \u201cbuild libs first\u201d friction).<\/li><li>Libraries are part of the same repo and not independently published.<\/li><li>You want the host app to compile against sources.<\/li><\/ul>\n\n\n\n<h5>Use&nbsp;<code>dist\/...<\/code>&nbsp;when:<\/h5>\n\n\n\n<ul><li>You want to treat libs like&nbsp;<strong>compiled packages<\/strong>&nbsp;(closer to what consumers get).<\/li><li>Your pipeline explicitly builds libs first (CI\/build orchestrations).<\/li><li>You want to ensure nothing outside the packaged output is accidentally consumed.<\/li><\/ul>\n\n\n\n<h5><span class=\"has-inline-color has-vivid-cyan-blue-color\">5.2 Dev note: build libs (when needed)<\/span><\/h5>\n\n\n\n<p>In many setups, <code>ng serve<\/code> will compile what it needs, but if you see resolution errors, build the libs once:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ng build topology\nng build bri\nng serve itop-topology-ui\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>6)&nbsp;<code>angular.json<\/code>&nbsp;essentials (projects, build\/test targets)<\/h4>\n\n\n\n<p>When we run <code>ng generate library ...<\/code>, Angular CLI updates <code>angular.json<\/code> automatically.<\/p>\n\n\n\n<p>We should verify we have entries like:<\/p>\n\n\n\n<ul><li><code>projects.itop-topology-ui.architect.build<\/code><\/li><li><code>projects.itop-topology-ui.architect.test<\/code><\/li><li><code>projects.topology.architect.build<\/code><\/li><li><code>projects.topology.architect.test<\/code><\/li><li><code>projects.bri.architect.build<\/code><\/li><li><code>projects.bri.architect.test<\/code><\/li><\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"projects\": {\n    \"itop-topology-ui\": {\n      \"projectType\": \"application\",\n      \"architect\": {\n        \"build\": { \"...\": \"...\" },\n        \"test\": { \"...\": \"...\" }\n      }\n    },\n    \"topology\": {\n      \"projectType\": \"library\",\n      \"architect\": {\n        \"build\": { \"...\": \"...\" },\n        \"test\": { \"...\": \"...\" }\n      }\n    },\n    \"bri\": {\n      \"projectType\": \"library\",\n      \"architect\": {\n        \"build\": { \"...\": \"...\" },\n        \"test\": { \"...\": \"...\" }\n      }\n    }\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>If your CI requires a dedicated \u201cci\u201d configuration, you can add test configurations under each project:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\"test\": {\n  \"options\": { \"...\": \"...\" },\n  \"configurations\": {\n    \"ci\": {\n      \"watch\": false,\n      \"codeCoverage\": true\n    }\n  }\n}\n<\/code><\/pre>\n\n\n\n<p>PS: (Exact keys depend on your Angular\/Karma setup, but the shape above is the common approach.)<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>7) Testing &amp; coverage per project<\/h4>\n\n\n\n<p>We can run tests per project:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ng test itop-topology-ui --code-coverage\nng test topology --code-coverage\nng test bri --code-coverage\n<\/code><\/pre>\n\n\n\n<p>In CI we typically run them all (and optionally merge coverage afterwards).<\/p>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<p><\/p>\n\n\n\n<h4>8) Recommended workspace conventions (to keep boundaries clean)<\/h4>\n\n\n\n<ul><li><strong>Never import from the host app into libraries<\/strong><br>e.g. avoid&nbsp;<code>import ... from 'projects\/itop-topology-ui\/src\/app\/...'<\/code><\/li><li>Put cross-cutting code in dedicated libs:<ul><li><code>shared-ui<\/code>,&nbsp;<code>shared-models<\/code>,&nbsp;<code>auth<\/code>,&nbsp;<code>permissions<\/code>, etc.<\/li><\/ul><\/li><li>Expose only what\u2019s needed via each lib\u2019s&nbsp;<code>public-api.ts<\/code>.<\/li><\/ul>\n\n\n\n<hr class=\"wp-block-separator\"\/>\n\n\n\n<h4>9) Common gotchas (and how to avoid them)<\/h4>\n\n\n\n<ol><li><strong>Path mapping points to&nbsp;<code>dist\/&lt;lib&gt;<\/code><\/strong><br>If imports like&nbsp;<code>import('topology')<\/code>&nbsp;fail, build the lib once or ensure the&nbsp;<code>paths<\/code>&nbsp;are correct.<\/li><li><strong>Deep imports<\/strong><br>Don\u2019t import&nbsp;<code>topology\/src\/lib\/...<\/code>&nbsp;from the app; export from&nbsp;<code>public-api.ts<\/code>.<\/li><li><strong>Routing duplication<\/strong><br>Keep feature routes inside the library module (via&nbsp;<code>RouterModule.forChild<\/code>) and only mount them in the host.<\/li><\/ol>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This guide shows how to create an Angular CLI workspace where our \u201cfeatures\u201d 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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":1965,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[37],"tags":[1112,1108,798,1110,569,1115,1111,1116,1109,1114,1113],"_links":{"self":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/4235"}],"collection":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/comments?post=4235"}],"version-history":[{"count":3,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/4235\/revisions"}],"predecessor-version":[{"id":4238,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/posts\/4235\/revisions\/4238"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/media\/1965"}],"wp:attachment":[{"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/media?parent=4235"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/categories?post=4235"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nguenkam.com\/blog\/index.php\/wp-json\/wp\/v2\/tags?post=4235"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}