---
title: "Getting Started with Standalone Components in Angular (Complete Modern Guide)"
date: "2022-11-02"
slug: "head-start-with-standalone-components-in-angular-15"
author: "Dany Paredes"
canonical: "https://danywalls.com/head-start-with-standalone-components-in-angular-15"
description: "Learn how to build module-less Angular applications using Standalone Components. Explore bootstrapApplication, app.config.ts, lazy loading with loadComponent, and Vitest unit testing."
---


When Angular initially introduced Standalone Components, it marked the beginning of a major architectural shift away from the complex `NgModule` system. Today, Standalone Components are the **official default** in Angular (v17, v18, and v19+).

By eliminating `NgModule` declarations, Standalone Components simplify dependency management, speed up compilation, make lazy loading straightforward, and improve developer onboarding.

> ⚡ **Using Modern Angular (v17+)?** Jump directly to [Modern Standalone Architecture: `app.config.ts`, Signals, and Vitest](#modern-standalone-architecture-appconfigts-signals-and-vitest) to see the latest standard project structure.

In this guide, we will explore how to build, import, route, and unit test Standalone Components in modern Angular.

Let's begin by generating our first Standalone Component.

---

## 1. Creating a Standalone Component 🧩

In modern Angular CLI (`ng new`), all components are standalone by default.

If you generate a component via CLI:

```bash
ng generate component components/user-card
```

The generated component declares its own dependencies inside its `@Component` metadata:

```typescript
// user-card.component.ts
import { Component, input, output } from '@angular/core';

export interface User {
  id: string;
  name: string;
  email: string;
}

@Component({
  selector: 'app-user-card',
  standalone: true, // 👈 Default in modern Angular
  imports: [],      // 👈 Directly declare other components, directives, or pipes
  template: `
    <div class="user-card p-4 border rounded-xl shadow-xs">
      <h3 class="font-bold">{{ user().name }}</h3>
      <p class="text-neutral-500">{{ user().email }}</p>
      <button (click)="remove.emit(user().id)" class="btn-danger mt-2">
        Remove
      </button>
    </div>
  `
})
export class UserCardComponent {
  readonly user = input.required<User>();
  readonly remove = output<string>();
}
```

Now let's see how to use one Standalone Component inside another.

---

## 2. Importing Standalone Components into Other Components 🔗

When a parent component needs to render a child standalone component, you add the child directly to the parent's `imports: [...]` array:

```typescript
// user-list.component.ts
import { Component, signal } from '@angular/core';
import { UserCardComponent, User } from '../user-card/user-card.component';

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [UserCardComponent], // 👈 Direct dependency declaration
  template: `
    <section class="user-list space-y-4">
      <h2>Active Team Members</h2>

      @for (member of members(); track member.id) {
        <app-user-card [user]="member" (remove)="onRemoveMember($event)" />
      } @empty {
        <p class="text-neutral-500">No members found.</p>
      }
    </section>
  `
})
export class UserListComponent {
  readonly members = signal<User[]>([
    { id: '1', name: 'Dany Paredes', email: 'dany@example.com' },
    { id: '2', name: 'Alex Johnson', email: 'alex@example.com' }
  ]);

  onRemoveMember(id: string) {
    this.members.update(list => list.filter(u => u.id !== id));
  }
}
```

Notice that you do **not** need `CommonModule` just for loops or conditionals—modern Angular `@for` and `@if` built-in control flow works natively everywhere!

Now let's look at how routing and lazy loading work with Standalone Components.

---

## 3. Route Configuration & Lazy Loading with `loadComponent` 🚀

In module-based Angular, lazy loading required a separate `NgModule` file with `loadChildren`. With Standalone Components, you lazy load individual components directly using **`loadComponent`**:

```typescript
// app.routes.ts
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    redirectTo: 'users',
    pathMatch: 'full'
  },
  {
    path: 'users',
    // Lazy load the standalone component on demand!
    loadComponent: () => import('./components/user-list/user-list.component')
      .then(m => m.UserListComponent)
  },
  {
    path: 'settings',
    loadComponent: () => import('./pages/settings/settings.component')
      .then(m => m.SettingsComponent)
  }
];
```

The Angular bundler will automatically create a dedicated JavaScript chunk for each route.

Now let's see how the entire application bootstraps without an `AppModule`.

---

## 4. Modern Standalone Architecture: `app.config.ts`, Signals, and Vitest ⚡

In modern Angular, `AppModule` is completely gone. Applications bootstrap using **`bootstrapApplication`** in `main.ts`, configured via `app.config.ts`:

### `app.config.ts`: Application Providers

```typescript
// app.config.ts
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withFetch } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideZoneChangeDetection({ eventCoalescing: true }),
    provideRouter(routes, withComponentInputBinding()),
    provideHttpClient(withFetch())
  ]
};
```

### `main.ts`: Clean Bootstrap

```typescript
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));
```

Now let's see how easy it is to write unit tests for Standalone Components using Vitest.

---

## 5. Testing Standalone Components with Vitest 🧪

Because Standalone Components declare their own dependencies, writing unit tests in **Vitest** is fast and requires zero module setup:

```typescript
// user-card.component.spec.ts (Vitest)
import { describe, it, expect, beforeEach } from 'vitest';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UserCardComponent, User } from './user-card.component';
import { ComponentRef } from '@angular/core';

describe('UserCardComponent with Vitest', () => {
  let component: UserCardComponent;
  let componentRef: ComponentRef<UserCardComponent>;
  let fixture: ComponentFixture<UserCardComponent>;

  const mockUser: User = { id: '101', name: 'Dany Paredes', email: 'dany@example.com' };

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserCardComponent] // 👈 Import standalone component directly!
    }).compileComponents();

    fixture = TestBed.createComponent(UserCardComponent);
    component = fixture.componentInstance;
    componentRef = fixture.componentRef;

    // Set signal input value
    componentRef.setInput('user', mockUser);
    fixture.detectChanges();
  });

  it('should render user information correctly', () => {
    const el = fixture.nativeElement as HTMLElement;
    expect(el.querySelector('h3')?.textContent).toBe('Dany Paredes');
    expect(el.querySelector('p')?.textContent).toBe('dany@example.com');
  });

  it('should emit remove event with user ID when clicked', () => {
    let emittedId = '';
    component.remove.subscribe((id) => (emittedId = id));

    const button = fixture.nativeElement.querySelector('button');
    button.click();

    expect(emittedId).toBe('101');
  });
});
```

---

## Legacy `NgModule` vs Modern Standalone Architecture 📊

| Aspect | Legacy Angular (v2 – v16) | Modern Angular (v17+) |
| :--- | :--- | :--- |
| **Component Registration** | Must be declared in an `@NgModule` | `standalone: true` (Default) |
| **Dependency Imports** | Injected via `NgModule.imports` | Declared directly in `@Component.imports` |
| **Application Bootstrap** | `platformBrowserDynamic().bootstrapModule(AppModule)` | `bootstrapApplication(AppComponent, appConfig)` |
| **Lazy Loading** | `loadChildren: () => import(...).then(m => m.MyModule)` | `loadComponent: () => import(...).then(m => m.MyComponent)` |
| **Unit Testing** | Heavy `TestBed` with module fixtures | Lightweight `TestBed` or Vitest directly |

---

## Recap 🛠️

Standalone Components make Angular cleaner and faster:
1. Components, directives, and pipes declare their own dependencies directly via `imports: [...]`.
2. Use **`bootstrapApplication`** and **`app.config.ts`** to configure providers cleanly.
3. Use **`loadComponent`** for instant route-level lazy loading without intermediary routing modules.
4. Test Standalone Components effortlessly using **Vitest**.

For more modern Angular guides, check out my articles on [How to Use ng-template, ng-container, and ng-content](/how-to-get-and-use-ng-template-ng-container-and-ng-content) and [How to Use Route Parameters with Signals](/learn-route-parameters-in-angular-with-example)!

