| |

Angular 114 🅰️ Libraries and Workspaces

⏱️ Estimated read: 15 min | |

Angular libraries are reusable collections of components, services, directives, and utilities that can be shared across applications. They are the mechanism by which code is factored out of a single application into a package that other projects can consume, whether those projects live in the same repository or are published to a package registry. A workspace, in Angular’s terminology, is the container that holds one or more applications and libraries together, with shared configuration and dependency management.

The decision to extract code into a library is architectural, not merely mechanical. It determines what code is public, what remains private, how versions are managed, and how changes propagate. A component that lives inside an application can be changed freely; a component that lives in a published library has consumers who depend on its behavior. Understanding the library system means understanding the boundary between reusable code and application-specific code.

This chapter covers the Angular workspace structure, creating and building libraries, the public API surface, consuming libraries within and outside a workspace, publishing to npm, and the relationship between libraries, path mappings, and build artifacts.

Key point: An Angular library is a project with a public API defined by its index.ts; a workspace is the container that holds applications and libraries with shared configuration. Libraries can be consumed locally via path mappings or published to npm for external use.


Why libraries and workspaces exist

The duplication problem. When several applications share UI components, services, or data models, copying the code into each application creates multiple sources of truth. A bug fix must be applied in every copy. A design change must be replicated everywhere. Libraries eliminate duplication by making the shared code a single project that every application imports.

The boundary problem. Not everything in an application should be public. Internal services, experimental components, and implementation details should not be importable by other projects. Angular libraries enforce a boundary through their public API: only what is exported from the library’s index.ts is accessible. Everything else is private to the library.

The workspace cohesion problem. A collection of unrelated repositories makes it difficult to coordinate changes across applications and libraries. When a library changes, every dependent application must update its dependency version, rebuild, and test. A workspace places them in the same repository with shared dependencies, so a change to a library is immediately visible to the applications that use it, and a single build can verify that nothing broke.

The publishing problem. Some libraries are meant for external consumption. A component library, a state management utility, or an API client might be published to npm for other teams or the public to use. Angular’s library build process produces distributable packages with proper metadata, multiple module formats, and TypeScript declarations, ready for publication.

The versioning problem. Libraries that are published have versions. A breaking change to a component’s API requires a major version bump. A new feature requires a minor version. A bug fix requires a patch. The workspace structure and Angular’s build tooling support this by producing packages with standard npm versioning and by allowing internal libraries to be versioned independently or in lockstep, depending on the project’s needs.


a. The Angular workspace structure

An Angular workspace is created with ng new using the --no-create-application flag, or it is the structure that results from ng new itself, which creates a workspace containing a single application. The workspace root contains angular.json, package.json, tsconfig.json, and a projects/ or src/ directory, depending on the version .

A workspace with multiple projects organizes them under a projects/ folder by default:

my-workspace/
├── angular.json
├── package.json
├── tsconfig.json
├── projects/
│   ├── my-app/
│   │   ├── src/
│   │   └── project.json (or configuration in angular.json)
│   └── my-lib/
│       ├── src/
│       │   ├── lib/
│       │   └── public-api.ts
│       └── project.json
└── node_modules/

The angular.json file contains configuration for every project in the workspace. Each project has a projectType — application or library — and a root that points to its directory. The architect section (or targets in newer versions) defines the build, test, lint, and serve configurations for the project .

A key file for libraries is public-api.ts (or index.ts in older versions), which defines what the library exports. Only symbols exported from this file are accessible to consumers. This is the library’s public API contract .

b. Creating and building a library

A library is created with the ng generate library command:

ng generate library my-lib

This creates a projects/my-lib/ directory with a src/lib/ folder for components and services, a public-api.ts file that exports the public API, and configuration in angular.json or a separate project.json . The generated library includes a sample component and service, along with test configuration.

Building a library uses the ng build command with the library name:

ng build my-lib

The output goes to dist/my-lib/ by default. The build produces multiple formats: an FESM2022 bundle (flat ECMAScript module), a UMD bundle for legacy consumers, and TypeScript declaration files. The package.json in the output has exports and types fields that point to these artifacts .

A library can be built in watch mode during development:

ng build my-lib --watch

This rebuilds the library whenever source files change, allowing a consuming application to pick up changes without a manual rebuild. However, the application’s development server may need to be restarted or may pick up changes via the path mapping, depending on the Angular version and configuration.

c. The public API and path mappings

The public-api.ts file is the gateway to the library. Every symbol that consumers can import must be exported from this file:

// projects/my-lib/src/public-api.ts
export * from './lib/my-lib.component';
export * from './lib/my-lib.service';
export * from './lib/models/user.model';

Symbols not exported here are private to the library. A component that imports another component from within the library can do so directly via relative paths, but external consumers cannot access it.

For local consumption within the workspace, Angular adds a path mapping to tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "my-lib": ["dist/my-lib"],
      "my-lib/*": ["dist/my-lib/*"]
    }
  }
}

This mapping points to the built output in dist/, not to the source. This means the library must be built before the consuming application can import it. The alternative is to point the path mapping to the source directory, which allows development without rebuilding but is not the default configuration .

Some configurations use a source mapping for development and a build mapping for production. The ng-packagr tool, which Angular uses to build libraries, supports this through the dest and source options in the library’s ng-package.json .

d. Consuming a library within a workspace

Once a library is built and the path mapping is in place, an application in the same workspace imports from it by name:

import { MyLibComponent } from 'my-lib';

The application’s build process resolves the import through the path mapping to the built library in dist/. The library does not need to be published to npm for local consumption.

If the library changes, it must be rebuilt for the application to see the changes. This is the primary friction point of local library development. Some teams use ng build my-lib --watch in one terminal and ng serve my-app in another, but the application’s dev server may not always pick up the rebuilt library. The npm link approach and the workspace path mapping both have limitations in this regard .

For libraries that are developed in parallel with an application, a common pattern is to use TypeScript path mappings that point to the library’s source instead of its build output. This allows the application’s dev server to see source changes immediately, at the cost of losing the build step’s validation.

e. Publishing a library to npm

Publishing a library makes it available to projects outside the workspace. The build output in dist/my-lib/ is a complete npm package with a package.json, bundled JavaScript, and TypeScript declarations. Publishing uses the standard npm workflow:

ng build my-lib
cd dist/my-lib
npm publish

Before publishing, the library’s package.json must have the correct name, version, and peerDependencies. Angular libraries typically declare Angular itself as a peer dependency, along with any other libraries the consumer must provide. This avoids bundling multiple copies of Angular and ensures that the consumer’s version is used .

The version field must be incremented for each publish, following semantic versioning. Breaking changes require a major version, new features a minor version, and bug fixes a patch version. Some workspaces automate this with tools like nx release or standard-version, which derive the version from commit messages.

Publishing a scoped package uses an npm scope:

{
  "name": "@myorg/my-lib",
  "version": "1.0.0"
}

Scoped packages require access configuration for the scope, either public or restricted, and the --access public flag on first publish for public scopes .

f. Library build formats and configuration

The Angular library build, powered by ng-packagr, produces several formats. The primary format is FESM2022 (Flat ECMAScript Module), a single file that bundles all the library’s modules into one ES module. This is the format modern bundlers consume. A UMD bundle is also produced for environments that require it, though UMD is increasingly rare .

TypeScript declarations (.d.ts files) are generated alongside the JavaScript. These declarations provide type information to consumers and are essential for a good developer experience. The types field in the output package.json points to the entry declaration file.

The ng-package.json file configures the library build. The dest field sets the output directory, and the lib.entryFile field specifies the public API file. The allowedNonPeerDependencies field lists dependencies that are bundled rather than treated as peer dependencies. By default, Angular libraries treat Angular and RxJS as peer dependencies and do not bundle them .


Complete Example Session

# ============================================
# PART 1: CREATE A WORKSPACE WITHOUT APPLICATION
# ============================================
# A workspace can contain only libraries.

ng new my-workspace --no-create-application
cd my-workspace
# ============================================
# PART 2: CREATE A LIBRARY
# ============================================
# Generate a library in the workspace.

ng generate library my-lib
# Creates projects/my-lib/ with public-api.ts
# ============================================
# PART 3: EXAMINE THE LIBRARY STRUCTURE
# ============================================
# The library has a source folder and public API.

ls projects/my-lib/src
# lib/  public-api.ts
// ============================================
// PART 4: DEFINE A COMPONENT IN THE LIBRARY
// ============================================
// projects/my-lib/src/lib/greeting.component.ts

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

@Component({
  selector: 'lib-greeting',
  template: '<h1>Hello, {{ name }}!</h1>',
  standalone: true,
})
export class GreetingComponent {
  @Input() name = 'World';
}
// ============================================
// PART 5: EXPORT FROM PUBLIC API
// ============================================
// projects/my-lib/src/public-api.ts

export * from './lib/greeting.component';
# ============================================
# PART 6: BUILD THE LIBRARY
# ============================================
# Output goes to dist/my-lib/.

ng build my-lib
# dist/my-lib/ contains package.json, FESM, .d.ts files
# ============================================
# PART 7: CREATE AN APPLICATION IN THE WORKSPACE
# ============================================
# Applications and libraries coexist in one workspace.

ng generate application my-app
// ============================================
// PART 8: CONSUME THE LIBRARY IN THE APP
// ============================================
// The path mapping resolves 'my-lib' to dist/my-lib.

// projects/my-app/src/app/app.component.ts
import { GreetingComponent } from 'my-lib';

@Component({
  selector: 'app-root',
  template: '<lib-greeting name="Angular" />',
  imports: [GreetingComponent],
})
export class AppComponent {}
# ============================================
# PART 9: BUILD THE LIBRARY IN WATCH MODE
# ============================================
# Rebuilds on source changes for development.

ng build my-lib --watch
# ============================================
# PART 10: PUBLISH THE LIBRARY TO NPM
# ============================================
# Build, then publish from the dist folder.

ng build my-lib
cd dist/my-lib
npm publish --access public

These ten parts cover workspace creation, library generation, component definition, public API export, library build, application creation, library consumption, watch mode, and publishing. The workflow is linear: create the library, define its API, build it, consume it, and optionally publish it.


Quick Reference

Workspace Commands

CommandPurpose
ng new <name> --no-create-applicationCreate workspace without app
ng generate library <name>Create a library
ng generate application <name>Create an application
ng build <lib>Build a library
ng build <lib> --watchBuild and watch for changes
ng test <lib>Run library tests
ng lint <lib>Lint library code

Library Project Structure

PathPurpose
projects/<lib>/src/lib/Components, services, models
projects/<lib>/src/public-api.tsPublic API exports
projects/<lib>/ng-package.jsonLibrary build configuration
projects/<lib>/package.jsonLibrary metadata for publishing
dist/<lib>/Build output

Build Output Formats

FormatPurpose
FESM2022Modern bundlers (ES modules)
UMDLegacy environments
.d.tsTypeScript declarations
package.jsonEntry points and metadata

Path Mapping

MappingTarget
my-libdist/my-lib
my-lib/*dist/my-lib/*

Best Practices

✅ Do This:

export * from './lib/greeting.component';        // Export from public-api
ng build my-lib --watch                           // Watch during development
npm publish --access public                       // Public scoped package
// package.json
"peerDependencies": {
  "@angular/core": "^20.0.0"                      // Angular as peer
}

❌ Don’t Do This:

// Do not export internal implementation details
export * from './lib/internal/helper';            // ❌ Reveals private code
"dependencies": {
  "@angular/core": "^20.0.0"                      // ❌ Bundles Angular
}

Common Pitfalls

PitfallWhy It HappensFix
Import not foundLibrary not builtng build my-lib before consuming
Changes not reflectedPath mapping points to distRebuild library or use source mapping
Publish failsVersion already existsIncrement version in library package.json
Peer dependency warningAngular version mismatchAlign Angular versions in workspace
Private symbols accessibleExported through public-apiRemove from public-api.ts
Library test failsTest configuration missingVerify test target in project config

Real-World Examples

1. Generate a Library with a Prefix

ng generate library ui-components --prefix=ui

2. Export Multiple Symbols

// public-api.ts
export * from './lib/button/button.component';
export * from './lib/card/card.component';
export * from './lib/services/theme.service';

3. Build All Libraries

ng build my-lib
ng build my-other-lib

4. Library with Peer Dependencies

{
  "peerDependencies": {
    "@angular/core": "^20.0.0",
    "@angular/common": "^20.0.0",
    "rxjs": "^7.8.0"
  }
}

5. Configure ng-package.json

{
  "dest": "../../dist/my-lib",
  "lib": {
    "entryFile": "src/public-api.ts"
  }
}

6. Publish a Scoped Package

npm publish --access public

7. Use npm link for External Testing

cd dist/my-lib
npm link
cd /path/to/consumer
npm link my-lib

8. Test the Library

ng test my-lib

9. Lint the Library

ng lint my-lib

10. Generate a Service in the Library

ng generate service services/data --project=my-lib

Visual

Workspace Structure

┌──────────────────────────────────────────────────────────────┐
│  ANGULAR WORKSPACE WITH APPLICATION AND LIBRARY              │
│                                                              │
│  my-workspace/                                               │
│  ├── angular.json          ← All project configurations      │
│  ├── package.json          ← Shared dependencies             │
│  ├── tsconfig.json         ← Path mappings                   │
│  │                                                           │
│  ├── projects/                                               │
│  │   ├── my-app/                                             │
│  │   │   ├── src/                                            │
│  │   │   └── project.json                                    │
│  │   │                                                       │
│  │   └── my-lib/                                             │
│  │       ├── src/                                            │
│  │       │   ├── lib/          ← Components, services        │
│  │       │   └── public-api.ts ← Public API                  │
│  │       ├── ng-package.json   ← Build config                │
│  │       └── package.json      ← Publish metadata            │
│  │                                                           │
│  └── dist/                                                   │
│      └── my-lib/             ← Build output                  │
│          ├── package.json                                    │
│          ├── fesm2022/                                       │
│          └── types/                                          │
└──────────────────────────────────────────────────────────────┘

Public API Boundary

┌──────────────────────────────────────────────────────────────┐
│  PUBLIC API CONTROLS WHAT CONSUMERS CAN IMPORT               │
│                                                              │
│  Library source:                                             │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  lib/button.component.ts       ──▶ exported            │  │
│  │  lib/card.component.ts         ──▶ exported            │  │
│  │  lib/internal/helper.ts        ──▶ NOT exported        │  │
│  │  lib/internal/state.ts         ──▶ NOT exported        │  │
│  └────────────────────────────────────────────────────────┘  │
│                          │                                   │
│                          ▼                                   │
│  public-api.ts:                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  export * from './lib/button.component';               │  │
│  │  export * from './lib/card.component';                 │  │
│  └────────────────────────────────────────────────────────┘  │
│                          │                                   │
│                          ▼                                   │
│  Consumer can import:                                        │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  import { ButtonComponent } from 'my-lib'; ✓           │  │
│  │  import { helper } from 'my-lib/internal/helper'; ✗    │  │
│  └────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────┘

Library Build Pipeline

┌──────────────────────────────────────────────────────────────┐
│  ng build my-lib → dist/my-lib/                              │
│                                                              │
│  Source TypeScript                                           │
│       │                                                      │
│       ▼                                                      │
│  ng-packagr                                                  │
│       │                                                      │
│       ├── Compile TypeScript                                 │
│       ├── Bundle with Rollup/esbuild                         │
│       ├── Generate declarations                              │
│       └── Write package.json for distribution                │
│       │                                                      │
│       ▼                                                      │
│  dist/my-lib/                                                │
│  ├── package.json         ← name, version, peerDeps          │
│  ├── fesm2022/            ← ES module bundle                 │
│  ├── esm2022/             ← Unbundled ES modules             │
│  ├── types/               ← .d.ts declaration files          │
│  └── README.md                                               │
└──────────────────────────────────────────────────────────────┘

Local Consumption via Path Mapping

┌──────────────────────────────────────────────────────────────┐
│  PATH MAPPING RESOLVES LIBRARY IMPORTS                       │
│                                                              │
│  tsconfig.json:                                              │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  "paths": {                                            │  │
│  │    "my-lib": ["dist/my-lib"],                          │  │
│  │    "my-lib/*": ["dist/my-lib/*"]                       │  │
│  │  }                                                     │  │
│  └────────────────────────────────────────────────────────┘  │
│                          │                                   │
│                          ▼                                   │
│  Application code:                                           │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  import { GreetingComponent } from 'my-lib';           │  │
│  └────────────────────────────────────────────────────────┘  │
│                          │                                   │
│                          ▼                                   │
│  Resolves to:                                                │
│  ┌────────────────────────────────────────────────────────┐  │
│  │  dist/my-lib/index.d.ts and bundled JS                 │  │
│  └────────────────────────────────────────────────────────┘  │
│                                                              │
│  The library must be built before the app can import it.     │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
WorkspaceContainer for applications and libraries
Library creationng generate library <name>
Library sourceprojects/<lib>/src/lib/
Public APIprojects/<lib>/src/public-api.ts
Library buildng build <lib>
Build outputdist/<lib>/
Build toolng-packagr
Output formatsFESM2022, UMD, .d.ts
Path mappingtsconfig.json paths
Publishingnpm publish from dist/<lib>/
Peer dependenciesAngular, RxJS declared as peers

Key takeaways:

  • A library is a project with a public API. Only symbols exported from public-api.ts are accessible to consumers; everything else is private.
  • A workspace holds applications and libraries together. They share dependencies, configuration, and a single node_modules, simplifying coordination.
  • Libraries must be built before local consumption. The default path mapping points to dist/, so ng build <lib> is required before an application can import it.
  • Watch mode rebuilds the library on changes. ng build <lib> --watch keeps the output current, though the consuming application may need a restart to pick up changes.
  • Publishing uses the standard npm workflow. The build output in dist/ is a complete package ready for npm publish .
  • Angular and RxJS are peer dependencies. Libraries declare them as peers rather than bundling them, ensuring the consumer’s version is used.
  • The public API is a contract. Once published, changing exported symbols is a breaking change requiring a major version bump.

Remember: Angular libraries and workspaces are the mechanism for sharing code across projects. A workspace is the container; a library is the reusable project within it. The public API defined by public-api.ts is the boundary between what the library offers and what it hides. Path mappings connect libraries to applications within the same workspace, while npm publishing connects them across repositories. The decision to extract code into a library should be driven by reuse and boundary needs: if code is used by multiple applications or has a clear public interface, a library is appropriate. If it is application-specific and unlikely to be shared, keeping it in the application is simpler. Understanding this trade-off is what makes the library system useful rather than ceremonial.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!