npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ng-arch-kit

v1.0.0

Published

Angular Schematics collection for feature-first architecture with Standalone Components and Signal Store

Readme

ng-arch-kit

Angular Schematics collection for generating feature-first architecture with Standalone Components and Signal Store

npm version License: MIT

Overview

ng-arch-kit is a powerful Angular Schematics collection that generates a complete, scalable feature-first architecture for Angular CLI applications. It promotes best practices including:

  • 🏗️ Standalone Components - Modern Angular architecture without NgModules
  • 📁 Feature-First Organization - Logical grouping by business domain
  • 🔄 Signal Store - State management with @ngrx/signals
  • 🎭 Smart/Dumb Components - Clear separation of container and presentational components
  • 🚧 Architectural Boundaries - Enforced layer dependencies via ESLint

Installation

npm install ng-arch-kit --save-dev

# Peer dependencies
npm install @ngrx/signals @ngrx/operators

Quick Start

Generate a complete feature:

ng generate ng-arch-kit:feature library books \
  --entity Book \
  --route books \
  --withFacade \
  --withUi list,filters \
  --crud

This creates the following structure:

src/app/features/library/books/
├── feature/
│   ├── books.routes.ts          # Feature routes
│   ├── books-page.container.ts  # Smart container component
│   ├── books-detail.container.ts
│   ├── books-shell.component.ts
│   └── index.ts
│
├── data-access/
│   ├── +state/
│   │   ├── books.store.ts       # Signal Store
│   │   └── books.facade.ts      # Optional facade layer
│   ├── books.api.ts             # HTTP API service
│   ├── books.models.ts          # TypeScript interfaces
│   └── index.ts
│
├── ui/
│   ├── books-list.component.ts  # Dumb/presentational
│   ├── books-filters.component.ts
│   └── index.ts
│
└── util/
    ├── books.query.ts           # Pure utility functions
    └── index.ts

Architecture

Layer Dependencies

┌─────────────────────────────────────────────────┐
│                   FEATURE                        │
│  (Smart/Container Components, Routes, Shell)     │
│                                                  │
│    ┌──────────────┐  ┌──────────┐  ┌──────────┐ │
│    │ data-access  │  │    ui    │  │   util   │ │
│    └──────────────┘  └──────────┘  └──────────┘ │
└─────────────────────────────────────────────────┘
         │                  │              │
         ▼                  ▼              ▼
┌──────────────┐      ┌──────────┐   ┌──────────┐
│ data-access  │      │    ui    │   │   util   │
│   (Store,    │      │  (Dumb   │   │  (Pure   │
│   Facade,    │──────│ Comps)   │───│ Funcs)   │
│   API, DTOs) │      │          │   │          │
└──────────────┘      └──────────┘   └──────────┘
         │                               ▲
         └───────────────────────────────┘

Import Rules

| Layer | Can Import From | |-------------|---------------------------| | feature | data-access, ui, util | | data-access | util | | ui | util (NO store/facade!) | | util | external libs only |

Smart vs Dumb Components

Smart/Container Components (.container.ts)

Located in the feature/ folder, these components:

  • ✅ Inject the store or facade
  • ✅ React to route parameters
  • ✅ Trigger data loading effects
  • ✅ Handle user interactions
  • ✅ Pass data down to dumb components
@Component({
  selector: 'app-books-page',
  standalone: true,
  imports: [BooksListComponent, BooksFiltersComponent],
  template: `
    <app-books-filters 
      [activeFilters]="facade.filters()"
      (filtersChange)="onFiltersChange($event)">
    </app-books-filters>
    
    <app-books-list
      [books]="facade.filteredBooks()"
      (select)="onSelect($event)">
    </app-books-list>
  `
})
export class BooksPageComponent {
  protected readonly facade = inject(BooksFacade);
  // ...
}

Dumb/Presentational Components (.component.ts)

Located in the ui/ folder, these components:

  • ✅ Receive all data via @input() signals
  • ✅ Emit all events via @output()
  • ❌ NO store/facade injection
  • ❌ NO routing
  • ❌ NO HTTP calls
@Component({
  selector: 'app-books-list',
  standalone: true,
  template: `
    @for (book of books(); track book.id) {
      <div (click)="select.emit(book)">{{ book.name }}</div>
    }
  `
})
export class BooksListComponent {
  readonly books = input.required<Book[]>();
  readonly select = output<Book>();
}

Schematics

ng g ng-arch-kit:feature

Generate a complete feature with all layers.

ng g ng-arch-kit:feature <domain> <name> [options]

Arguments

| Argument | Description | |----------|-------------| | domain | The domain/module name (e.g., 'library', 'admin') | | name | The feature name (e.g., 'books', 'users') |

Options

| Option | Alias | Default | Description | |--------|-------|---------|-------------| | --entity | -e | Singular of name | Entity name (e.g., 'Book') | | --route | -r | Feature name | Route path | | --withFacade | -f | false | Generate facade layer | | --withUi | | 'list' | Comma-separated UI components | | --crud | | true | Generate CRUD operations | | --skipTests | | false | Skip test file generation |

Examples

# Basic feature
ng g ng-arch-kit:feature admin users

# Feature with facade and multiple UI components
ng g ng-arch-kit:feature library books \
  --entity Book \
  --withFacade \
  --withUi list,filters,card

# Feature without CRUD
ng g ng-arch-kit:feature dashboard analytics --crud false

ng g ng-arch-kit:ui

Add a dumb/presentational UI component to an existing feature.

ng g ng-arch-kit:ui <domain> <feature> <name> [options]

Options

| Option | Description | |--------|-------------| | --entity | Entity type the component works with | | --inputs | Input properties (e.g., 'item:Book,selected:boolean') | | --outputs | Output events (e.g., 'select:Book,delete:string') |

Example

ng g ng-arch-kit:ui library books book-card \
  --inputs "book:Book,selected:boolean" \
  --outputs "select:Book,edit:Book"

ng g ng-arch-kit:route

Add a child route to an existing feature.

ng g ng-arch-kit:route <domain> <feature> <path> [options]

Options

| Option | Default | Description | |--------|---------|-------------| | --component | Derived from path | Component name | | --lazy | true | Lazy load the component | | --container | true | Generate as container/smart component |

Example

# Add edit route
ng g ng-arch-kit:route library books "edit/:id" --component books-edit

# Add a non-lazy presentational route
ng g ng-arch-kit:route library books "new" --lazy false --container false

Signal Store

The generated Signal Store includes:

State

interface BooksState {
  entities: Book[];
  selectedId: string | null;
  loading: boolean;
  error: string | null;
  filters: BookFilters;
}

Derived State (Computed Selectors)

withComputed((store) => ({
  selectedBook: computed(() => /* ... */),
  filteredBooks: computed(() => /* ... */),
  totalCount: computed(() => /* ... */),
  stats: computed(() => /* ... */),
}))

Actions with Async Effects

withMethods((store, api = inject(BooksApiService)) => ({
  load: rxMethod<void>(
    pipe(
      tap(() => patchState(store, { loading: true })),
      switchMap(() => api.getAll().pipe(
        takeUntil(cancelLoad$), // Request cancellation
        tapResponse({
          next: (entities) => patchState(store, { entities, loading: false }),
          error: (error) => patchState(store, { error: error.message, loading: false }),
        })
      ))
    )
  ),
  create: rxMethod<Book>(/* ... */),
  update: rxMethod<Book>(/* ... */),
  delete: rxMethod<string>(/* ... */),
}))

Optional Facade Layer

When using --withFacade, a facade class is generated that:

  1. Provides a simplified API - Intent-based methods like save() instead of create()/update()
  2. Exposes view models - Aggregated computed signals for components
  3. Decouples components from store - Easier testing and refactoring
@Injectable({ providedIn: 'root' })
export class BooksFacade {
  private readonly store = inject(BooksStore);

  // View model
  readonly vm = computed(() => ({
    books: this.store.entities(),
    filteredBooks: this.store.filteredBooks(),
    loading: this.store.loading(),
    error: this.store.error(),
  }));

  // Intent-based methods
  load(): void { this.store.load(); }
  save(book: Book): void {
    book.id ? this.store.update(book) : this.store.create(book);
  }
  delete(id: string): void { this.store.delete(id); }
}

ESLint Boundary Rules

Copy the provided ESLint configuration to enforce architectural boundaries:

cp node_modules/ng-arch-kit/eslint-boundaries.config.json .eslintrc.json

Or merge the rules into your existing ESLint configuration.

This enforces:

  • ❌ UI components cannot import from data-access
  • data-access cannot import from feature or ui
  • util cannot import from any app layer
  • ❌ No cross-feature imports

Route Auto-Registration

The feature schematic automatically patches src/app/app.routes.ts:

export const routes: Routes = [
  {
    path: 'books',
    loadChildren: () =>
      import('./features/library/books/feature')
        .then(m => m.BOOKS_ROUTES)
  },
  // Other routes...
];

Best Practices

1. Keep UI Components Pure

// ✅ Good - Dumb component
@Component({ /* ... */ })
export class BookCardComponent {
  readonly book = input.required<Book>();
  readonly select = output<Book>();
}

// ❌ Bad - Dumb component with dependencies
@Component({ /* ... */ })
export class BookCardComponent {
  private readonly store = inject(BooksStore); // Don't do this!
}

2. Use Barrel Files

Always import from the layer's index.ts:

// ✅ Good
import { BooksFacade, Book } from '../data-access';

// ❌ Bad
import { BooksFacade } from '../data-access/+state/books.facade';

3. Keep Business Logic in Store/Facade

// ✅ Store handles the logic
store.filteredBooks = computed(() => {
  return applyFilters(store.entities(), store.filters());
});

// Container component just uses it
template: `<app-list [items]="facade.filteredBooks()"></app-list>`

4. Use Computed Signals for Derived State

// ✅ Good - Computed from state
readonly stats = computed(() => ({
  total: this.store.entities().length,
  filtered: this.store.filteredBooks().length,
}));

// ❌ Bad - Calculating in template
template: `{{ store.entities().length }} items`

Naming Conventions

| Type | Pattern | Example | |------|---------|---------| | Smart/Container Component | *-page.container.ts, *-detail.container.ts | books-page.container.ts | | Dumb/UI Component | *.component.ts | books-list.component.ts | | Shell Component | *-shell.component.ts | books-shell.component.ts | | Store | *.store.ts | books.store.ts | | Facade | *.facade.ts | books.facade.ts | | API Service | *.api.ts | books.api.ts | | Models | *.models.ts | books.models.ts | | Utilities | *.query.ts | books.query.ts |

Code Quality Conventions

Directory Naming: The +state Prefix

The +state/ folder uses a + prefix convention from the Nx/NgRx ecosystem:

data-access/
  +state/              ← Internal state management
    books.store.ts
    books.facade.ts
  index.ts             ← Public API (barrel file)
  books.api.ts
  books.models.ts

Why +state?

  1. Signals "internal" - Files here shouldn't be imported directly from outside data-access
  2. Visual marker - The + clearly indicates "don't import from here directly"
  3. Sort order - Appears at the top of directory listings (before alphabetical folders)
  4. Convention - Widely used in Nx workspaces and NgRx projects

Barrel Files (index.ts)

Each layer exposes a public API through its index.ts file:

// data-access/index.ts - Public API
export { BooksStore } from './+state/books.store';
export { BooksFacade } from './+state/books.facade';
export type { Book, BookFilters } from './books.models';
export { BooksApiService } from './books.api';

Rules:

  • ✅ Always import from the barrel: import { Book } from '../data-access'
  • ❌ Never import internal paths: import { Book } from '../data-access/+state/books.store'

Type-Only Exports

Use export type for interfaces and type aliases to support isolatedModules:

// ✅ Correct
export type { Book, BookId, BookFilters } from './books.models';
export { isBook } from './books.models';  // Runtime value

// ❌ Causes errors with isolatedModules
export { Book, BookId, isBook } from './books.models';

Component Suffixes

| Suffix | Purpose | Location | Injects Store? | |--------|---------|----------|----------------| | .container.ts | Smart/container component | feature/ | ✅ Yes | | .component.ts | Dumb/presentational component | ui/ | ❌ No | | -shell.component.ts | Layout wrapper with router outlet | feature/ | Optional |

Output Naming

Avoid DOM event names for outputs to prevent conflicts:

// ✅ Good - Custom name
readonly itemSelect = output<Book>();
readonly itemDelete = output<string>();

// ❌ Avoid - Conflicts with DOM events
readonly select = output<Book>();  // 'select' is a DOM event
readonly change = output<any>();   // 'change' is a DOM event

Signal Store Patterns

Request Cancellation

Always cancel pending requests when starting new ones:

withMethods((store, api = inject(BooksApiService)) => {
  const cancelLoad$ = new Subject<void>();

  return {
    load: rxMethod<void>(
      pipe(
        tap(() => {
          cancelLoad$.next();  // Cancel previous request
          patchState(store, { loading: true });
        }),
        switchMap(() =>
          api.getAll().pipe(
            takeUntil(cancelLoad$),  // Allow cancellation
            tapResponse({ /* ... */ })
          )
        )
      )
    ),
  };
})

Entity ID Types

Use branded types for entity IDs to improve type safety:

// models.ts
export type BookId = string;  // Semantic alias

export interface Book {
  id: BookId;  // Clear intent
  name: string;
}

// Usage - prevents mixing different ID types
select(bookId: BookId): void { /* ... */ }

CSS/SCSS Conventions

Generated styles follow these patterns:

| Class Pattern | Purpose | |---------------|---------| | .{feature}-page | Page-level container | | .{feature}-list | List container | | .list-item | Individual list items | | .empty-state | No-data state | | .loading-indicator | Loading spinner | | .error-message | Error display | | .btn-primary, .btn-danger | Action buttons |

Accessibility

Generated components include accessibility features:

<!-- Keyboard navigation -->
<li
  tabindex="0"
  (click)="itemSelect.emit(item)"
  (keydown.enter)="itemSelect.emit(item)"
  (keydown.space)="$event.preventDefault(); itemSelect.emit(item)">

Testing Conventions

| File Pattern | Purpose | |--------------|---------| | *.spec.ts | Unit tests (Jasmine/Jest) | | *.cy.ts | Component tests (Cypress) |

Test files are co-located with implementation files.

Import Order

Organize imports in this order:

// 1. Angular core
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';

// 2. Angular modules
import { RouterModule } from '@angular/router';

// 3. Third-party libraries
import { signalStore } from '@ngrx/signals';

// 4. Local app imports (relative)
import { BooksFacade, Book } from '../data-access';
import { BooksListComponent } from '../ui';

Example App

See the /example directory for a complete working example with the library/books feature.

To run the example:

  1. Create a new Angular app: ng new my-app
  2. Navigate to the app: cd my-app
  3. Install dependencies: npm install @ngrx/signals @ngrx/operators
  4. Link ng-arch-kit: npm link ../path-to/ng-arch-kit
  5. Generate a feature: ng g ng-arch-kit:feature library books --withFacade --withUi list,filters
  6. Start the dev server: ng serve
  7. Navigate to http://localhost:4200/books

Troubleshooting

"Cannot find module '@ngrx/signals'"

Install the required peer dependencies:

npm install @ngrx/signals @ngrx/operators

"Could not find app.routes.ts"

Make sure you're using an Angular 17+ application with standalone components and file-based routing. The schematic looks for src/app/app.routes.ts.

Feature not loading

Verify the route was added to app.routes.ts and that the feature path is correct.

Contributing

Contributions are welcome! Please read our contributing guide for details.

License

MIT