How to create secondary entry points in Angular libraries for better modularity and smaller bundles
Ever wondered why @angular/common has both @angular/common and @angular/common/http? Or why some Angular libraries let you import specific features without dragging in the entire package? That's the magic of multiple entry points — and today, you're going to master this powerful pattern.
Picture this: You've built an amazing Angular library with utilities, components, and services. But when developers install it, they're forced to import everything, even if they only need one small feature. Their bundle size explodes, and suddenly your "helpful" library becomes a performance bottleneck. Sound familiar?
By the end of this article, you'll know exactly how to:
- Structure Angular libraries with multiple entry points for optimal tree-shaking
- Create secondary entry points that work seamlessly in both standard and Nx workspaces
- Reduce bundle sizes by up to 70% through smart library architecture
- Avoid the common pitfalls that plague multi-entry-point libraries
Let's dive in and transform your monolithic library into a lean, modular powerhouse.
What Are Entry Points in Angular Libraries?
Think of entry points as different doors into your library. Just like a building might have a main entrance, a side door, and a loading dock — each serving different purposes — your Angular library can expose multiple entry points for different features.
When you create a standard Angular library, you get one default entry point:
// Importing from main entry point
import { MyService } from '@mycompany/awesome-lib';
But with secondary entry points, you can offer more granular imports:
// Importing from secondary entry points
import { UtilityService } from '@mycompany/awesome-lib/utilities';
import { SharedComponent } from '@mycompany/awesome-lib/components';
The Angular team uses this pattern extensively. Take a look at Angular Material — you don't import the entire library; you cherry-pick what you need:
import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog';
Quick question for you: Have you ever hesitated to add a library to your project because you were worried about bundle size? Drop a comment below — I'd love to hear your experience!
Why Multiple Entry Points Matter
Before we jump into implementation, let's talk about why you should care. Here's what multiple entry points bring to the table:
The Good Stuff
- Surgical tree-shaking: Only the code that's actually imported makes it into the final bundle
- Faster builds: Smaller compilation units mean quicker build times
- Better organization: Logical separation of features makes your library more maintainable
- Gradual adoption: Developers can use parts of your library without committing to the whole thing
The Trade-offs
- More configuration: Each entry point needs its own setup
- Versioning complexity: All entry points share the same version number
- Publishing overhead: More moving parts means more things that can go wrong
Now that we're on the same page about the why, let's get our hands dirty with the how.
Step-by-Step: Creating Your First Secondary Entry Point
I'm going to walk you through creating a secondary entry point in a real Angular library. We'll build a library with a main entry point for core features and a secondary entry point for utilities.
Step 1: Generate Your Library
First, let's create a new Angular workspace and library:
ng new my-workspace --no-create-application
cd my-workspace
ng generate library @mycompany/awesome-lib
Your initial structure looks like this:
my-workspace/
├── projects/
│ └── mycompany/
│ └── awesome-lib/
│ ├── src/
│ │ ├── lib/
│ │ └── public-api.ts
│ ├── ng-package.json
│ └── package.json
Step 2: Create the Secondary Entry Point Folder
Navigate to your library folder and create a new directory for your secondary entry point:
cd projects/mycompany/awesome-lib
mkdir utilities
Step 3: Configure the Secondary Entry Point
Create a ng-package.json file inside the utilities folder:
// projects/mycompany/awesome-lib/utilities/ng-package.json
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "public-api.ts"
}
}
Step 4: Set Up the Public API
Create the public API file for your utilities:
// projects/mycompany/awesome-lib/utilities/public-api.ts
export * from './src/string-utils';
export * from './src/date-utils';
export * from './src/array-utils';
Step 5: Create Your Utility Services
Let's add some actual utilities. Here's a string utility service:
// projects/mycompany/awesome-lib/utilities/src/string-utils.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StringUtilsService {
capitalize(text: string): string {
if (!text) return '';
return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase();
}
truncate(text: string, maxLength: number, suffix = '...'): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength - suffix.length) + suffix;
}
slugify(text: string): string {
return text
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
}
And a date utility service:
// projects/mycompany/awesome-lib/utilities/src/date-utils.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class DateUtilsService {
formatRelativeTime(date: Date): string {
const now = new Date();
const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000);
if (diffInSeconds < 60) return 'just now';
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
return `${Math.floor(diffInSeconds / 86400)} days ago`;
}
isWeekend(date: Date): boolean {
const day = date.getDay();
return day === 0 || day === 6;
}
addBusinessDays(date: Date, days: number): Date {
const result = new Date(date);
let daysAdded = 0;
while (daysAdded < days) {
result.setDate(result.getDate() + 1);
if (!this.isWeekend(result)) {
daysAdded++;
}
}
return result;
}
}
Step 6: Update Package.json Exports
Modern Angular uses the exports field in package.json to define entry points. Update your library's package.json:
// projects/mycompany/awesome-lib/package.json
{
"name": "@mycompany/awesome-lib",
"version": "1.0.0",
"peerDependencies": {
"@angular/common": "^18.0.0",
"@angular/core": "^18.0.0"
},
"dependencies": {
"tslib": "^2.3.0"
},
"exports": {
".": {
"types": "./index.d.ts",
"esm2022": "./esm2022/mycompany-awesome-lib.mjs",
"esm": "./esm2022/mycompany-awesome-lib.mjs",
"default": "./fesm2022/mycompany-awesome-lib.mjs"
},
"./utilities": {
"types": "./utilities/index.d.ts",
"esm2022": "./esm2022/utilities/mycompany-awesome-lib-utilities.mjs",
"esm": "./esm2022/utilities/mycompany-awesome-lib-utilities.mjs",
"default": "./fesm2022/mycompany-awesome-lib-utilities.mjs"
}
},
"sideEffects": false
}
Step 7: Build and Verify
Build your library to ensure everything is configured correctly:
ng build @mycompany/awesome-lib
After building, check the dist folder. You should see separate bundles for each entry point:
dist/mycompany/awesome-lib/
├── fesm2022/
│ ├── mycompany-awesome-lib.mjs
│ └── mycompany-awesome-lib-utilities.mjs
├── utilities/
│ ├── index.d.ts
│ └── package.json
└── package.json
Implementing in an Nx Monorepo
Working with Nx? The process is slightly different but even more powerful. Nx provides generators that handle most of the boilerplate for you.
Generate a Library with Secondary Entry Points in Nx
npx nx generate @nx/angular:library my-lib --buildable --publishable --importPath=@mycompany/my-lib
# Add a secondary entry point
npx nx generate @nx/angular:library-secondary-entry-point --library=my-lib --name=utilities
Nx automatically configures the ng-package.json and updates your tsconfig paths. The structure looks like this:
libs/
└── my-lib/
├── src/
│ ├── index.ts
│ └── lib/
├── utilities/
│ ├── src/
│ │ ├── index.ts
│ │ └── lib/
│ └── ng-package.json
└── ng-package.json
Here's a question for you: Are you using Nx in your projects? What's your favorite feature? Let me know in the comments!
Consuming Your Multi-Entry Point Library
Now for the fun part — using your shiny new library in an Angular application!
Install Your Library
If you've published to npm:
npm install @mycompany/awesome-lib
Or link locally for development:
npm link @mycompany/awesome-lib
Import from Different Entry Points
In your Angular components, you can now import from specific entry points:
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { StringUtilsService } from '@mycompany/awesome-lib/utilities';
import { DateUtilsService } from '@mycompany/awesome-lib/utilities';
@Component({
selector: 'app-root',
template: `
<div class="container">
<h1>{{ title }}</h1>
<p>Slugified: {{ slug }}</p>
<p>Posted: {{ relativeTime }}</p>
</div>
`
})
export class AppComponent implements OnInit {
title = 'Using Multiple Entry Points';
slug = '';
relativeTime = '';
constructor(
private stringUtils: StringUtilsService,
private dateUtils: DateUtilsService
) {}
ngOnInit() {
this.slug = this.stringUtils.slugify(this.title);
this.relativeTime = this.dateUtils.formatRelativeTime(
new Date(Date.now() - 3600000)
);
}
}
The beauty here? If you only import from @mycompany/awesome-lib/utilities, the main library code never makes it into your bundle. Tree-shaking at its finest!
Writing Unit Tests for Your Library
Testing is crucial for library development. Let's add comprehensive tests for our utility services:
// string-utils.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { StringUtilsService } from './string-utils';
describe('StringUtilsService', () => {
let service: StringUtilsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(StringUtilsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
describe('capitalize', () => {
it('should capitalize first letter and lowercase rest', () => {
expect(service.capitalize('hello WORLD')).toBe('Hello world');
});
it('should handle empty strings', () => {
expect(service.capitalize('')).toBe('');
});
it('should handle single character strings', () => {
expect(service.capitalize('a')).toBe('A');
});
});
describe('truncate', () => {
it('should truncate long text', () => {
const text = 'This is a very long text that needs truncation';
expect(service.truncate(text, 20)).toBe('This is a very lo...');
});
it('should not truncate short text', () => {
expect(service.truncate('Short', 10)).toBe('Short');
});
it('should use custom suffix', () => {
expect(service.truncate('Long text here', 10, '→')).toBe('Long text→');
});
});
describe('slugify', () => {
it('should create URL-friendly slugs', () => {
expect(service.slugify('Hello World!')).toBe('hello-world');
expect(service.slugify(' Angular & TypeScript ')).toBe('angular-typescript');
expect(service.slugify('100% Awesome!!!')).toBe('100-awesome');
});
});
});
And for the date utilities:
// date-utils.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { DateUtilsService } from './date-utils';
describe('DateUtilsService', () => {
let service: DateUtilsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(DateUtilsService);
});
describe('formatRelativeTime', () => {
it('should format recent times as "just now"', () => {
const now = new Date();
expect(service.formatRelativeTime(now)).toBe('just now');
});
it('should format minutes ago correctly', () => {
const thirtyMinutesAgo = new Date(Date.now() - 30 * 60 * 1000);
expect(service.formatRelativeTime(thirtyMinutesAgo)).toBe('30 minutes ago');
});
it('should format hours ago correctly', () => {
const twoHoursAgo = new Date(Date.now() - 2 * 60 * 60 * 1000);
expect(service.formatRelativeTime(twoHoursAgo)).toBe('2 hours ago');
});
});
describe('isWeekend', () => {
it('should identify weekends correctly', () => {
const saturday = new Date('2024-01-06'); // Saturday
const sunday = new Date('2024-01-07'); // Sunday
const monday = new Date('2024-01-08'); // Monday
expect(service.isWeekend(saturday)).toBe(true);
expect(service.isWeekend(sunday)).toBe(true);
expect(service.isWeekend(monday)).toBe(false);
});
});
describe('addBusinessDays', () => {
it('should skip weekends when adding business days', () => {
const friday = new Date('2024-01-05'); // Friday
const result = service.addBusinessDays(friday, 3);
// Should be Wednesday (skipping Saturday and Sunday)
expect(result.getDay()).toBe(3);
expect(result.getDate()).toBe(10);
});
});
});
Run your tests with:
ng test @mycompany/awesome-lib
Troubleshooting Common Issues
Let's address the gotchas that trip up most developers:
Issue 1: "Cannot find module '@mycompany/awesome-lib/utilities'"
Solution: Check your package.json exports field. The path must match exactly:
"exports": {
"./utilities": { ... } // Note the "./" prefix
}
Issue 2: Build Fails with "Public API exports not found"
Solution: Ensure your public-api.ts file exists and exports are correct:
// Correct
export * from './src/my-service';
// Incorrect - missing 'src' folder
export * from './my-service';
Issue 3: TypeScript Can't Find Types
Solution: Update your tsconfig.json paths:
{
"compilerOptions": {
"paths": {
"@mycompany/awesome-lib": ["dist/mycompany/awesome-lib"],
"@mycompany/awesome-lib/*": ["dist/mycompany/awesome-lib/*"]
}
}
}
Issue 4: Entry Point Not Tree-Shaken
Solution: Add "sideEffects": false to your package.json. This tells bundlers that your code is side-effect free and safe to tree-shake.
Best Practices and When to Use Multiple Entry Points
Not every library needs multiple entry points. Here's my decision framework:
Use Multiple Entry Points When:
- Your library has distinct, independent feature sets
- Users typically only need a subset of your functionality
- Bundle size is a critical concern for your users
- You're building a utility library with many standalone functions
Keep a Single Entry Point When:
- Your library is small (< 50KB)
- All features are tightly coupled
- Users typically need everything you provide
- The added complexity isn't worth the marginal gains
Pro Tips from the Trenches
1. Group Related Features: Don't create an entry point for every single service. Group related functionality:
@mycompany/ui-kit/buttons
@mycompany/ui-kit/forms
@mycompany/ui-kit/layout
2. Version Everything Together: All entry points share the same version. Don't try to version them independently — it's a nightmare.
3. Document Import Paths: Make it crystal clear how to import from each entry point. Good documentation beats clever code every time.
4. Test Bundle Output: Use tools like webpack-bundle-analyzer to verify that tree-shaking actually works:
npm install -D webpack-bundle-analyzer
ng build --stats-json
npx webpack-bundle-analyzer dist/stats.json
Bonus Tip: Automating Entry Point Creation
Here's a quick Node.js script to generate new entry points automatically:
// scripts/create-entry-point.js
const fs = require('fs');
const path = require('path');
function createEntryPoint(name) {
const libPath = 'projects/mycompany/awesome-lib';
const entryPath = path.join(libPath, name);
// Create directory
fs.mkdirSync(entryPath, { recursive: true });
fs.mkdirSync(path.join(entryPath, 'src'), { recursive: true });
// Create ng-package.json
const ngPackageContent = {
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "public-api.ts"
}
};
fs.writeFileSync(
path.join(entryPath, 'ng-package.json'),
JSON.stringify(ngPackageContent, null, 2)
);
// Create public-api.ts
fs.writeFileSync(
path.join(entryPath, 'public-api.ts'),
`// Public API Surface of ${name}\n`
);
console.log(`Entry point '${name}' created successfully!`);
}
// Usage: node scripts/create-entry-point.js my-feature
createEntryPoint(process.argv[2]);
Recap: Your Entry Point Mastery Checklist
Let's recap what you've learned today:
- Entry points are doors into your library — each one provides access to specific features
- Secondary entry points enable surgical imports — users only bundle what they actually use
- Configuration is straightforward — ng-package.json, public-api.ts, and package.json exports
- Testing is non-negotiable — comprehensive tests ensure your library works reliably
- Not every library needs multiple entry points — use them when they solve real problems
You now have the knowledge to create professional-grade Angular libraries that respect your users' bundle sizes while maintaining clean architecture. The next time you build a library, you'll know exactly when and how to leverage multiple entry points.
What's Your Next Move?
What did you think? Have you struggled with large library bundles before? What approach are you most excited to try? Drop a comment below — I read every single one and often write follow-up articles based on your questions!
Found this helpful? If this article saved you from bundle bloat, hit that clap button! Seriously, every clap helps other developers discover these techniques. Go ahead, you can clap up to 50 times — I dare you to see how fast you can click!
Want more Angular deep dives? I publish practical Angular insights every week. Follow me here on Medium and never miss an article. Better yet, join my newsletter where I share exclusive tips and early access to new content before it goes public.
Your action items:
- Audit your existing libraries — do any need splitting into entry points?
- Try the techniques in this article on a real project
- Share your results in the comments
- Challenge a colleague to optimize their library architecture
Remember: great developers don't just write code that works — they write code that scales, performs, and respects their users' constraints. Now go forth and build something awesome!
Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
- 💼 LinkedIn — Let’s connect professionally
- 🎥 Threads — Short-form frontend insights
- 🐦 X (Twitter) — Developer banter + code snippets
- 👥 BlueSky — Stay up to date on frontend trends
- 🌟 GitHub Projects — Explore code in action
- 🌐 Website — Everything in one place
- 📚 Medium Blog — Long-form content and deep-dives
- 💬 Dev Blog — Free Long-form content and deep-dives
- ✉️ Substack — Weekly frontend stories & curated resources
- 🧩 Portfolio — Projects, talks, and recognitions
- ✍️ Hashnode — Developer blog posts & tech discussions
- ✍️ Reddit — Developer blog posts & tech discussions
Top comments (0)