Mert Özen Further With Every Line
Frontend Angular — #04

What Is an Angular NgModule? Declarations, Imports, Exports and Feature Modules

Mert Özen Oct 31, 2026 7 min 2 views
What Is an Angular NgModule? Declarations, Imports, Exports and Feature Modules

We open up NgModule, the skeleton of an Angular 12 app: declarations, imports, exports, providers, bootstrap, startup in main.ts, BrowserModule vs CommonModule, feature and shared modules.

Version note: The code and commands in this post are for Angular 12.

Components Need an Address Too

The ng generate component command we saw earlier quietly updated one more file every time it ran: app.module.ts. And when you deleted that line, the app complained with "is not a known element." So writing a component is not enough; you also have to tell Angular where that component lives.

Think of a company. Components are the employees, modules are the departments. Every employee is on the payroll of exactly one department. A department can get help from another one, but only if that department has made the help available. In Angular 12 these departments are called NgModules. In this post we will open one up and go through what sits inside it and why.

Reading AppModule Line by Line

Open src/app/app.module.ts in the project. This is what ng new generates:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

A familiar pattern: a plain TypeScript class with a decorator on top. Components had @Component; here we have @NgModule. The class body is empty; AppModule does nothing on its own. All the information lives in the object we pass to the decorator.

That object has five core fields: declarations, imports, exports, providers and bootstrap. You don't see exports in AppModule because the root module has no reason to hand anything to anyone. Let's go through the fields one at a time.

declarations: The Module's Staff

declarations is the list of components, directives and pipes that belong to this module. We will cover directives and pipes in upcoming posts; for now it's enough to know that, like components, they are pieces you use in templates.

Two rules apply here. First, a template can only use pieces that are declared in its own module or brought in from another module. Second, a component can appear in the declarations of only one module. Put the same component in two modules and the build stops with this error:

error NG6007: The Component 'ProductListComponent' is declared by more than one NgModule.

Back to the company: an employee can't be on two departments' payrolls at once. If they need to work in another department, their own department lends them out. That lending is called exports.

imports, exports and providers

imports is the list of other modules this module needs. AppModule has a single entry: BrowserModule. That import is what lets the app run in the browser and gives your templates access to the ready-made building blocks.

exports is the other side of the coin. Here a module states which of its own pieces it makes available to the outside. A component that is not exported is that module's private business; another module can't see it even if it imports the module.

So for a piece to be shared between two modules, both sides have to reach out. The giving module lists the piece in exports, and the receiving module adds the giving module to imports. If either is missing, the template gets stuck on the same "is not a known element" error.

A small detail people often mix up: the imports inside @NgModule is not the same thing as the TypeScript import lines at the top of the file. The lines at the top introduce files to TypeScript; the list in the decorator introduces modules to Angular. You need both.

The fourth field, providers, is the list of services registered at the module level. It's empty in a new project and often stays that way, because in Angular 12 services usually register themselves with @Injectable({ providedIn: 'root' }). We will cover services and dependency injection in upcoming posts.

bootstrap and main.ts: How the App Starts

The last field, bootstrap, only appears in the root module and tells Angular which component to render first when the app starts. In AppModule that is AppComponent. Angular looks for that component's selector, the <app-root> tag, in index.html and puts AppComponent there. This is exactly where the root of the component tree is decided.

So who runs AppModule? The answer is in src/main.ts:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

platformBrowserDynamic().bootstrapModule(AppModule)
  .catch(err => console.error(err));

The heart of it is the last statement. platformBrowserDynamic() prepares the browser platform the app will run on. bootstrapModule(AppModule) tells that platform "this module is the starting point." Angular reads AppModule's decorator, finds AppComponent in the bootstrap list and renders it. If anything fails along the way, catch logs it to the console.

The if block above it calls enableProdMode(), which turns off the extra development-mode checks in a production build. We will come back to environment files when we talk about build and deploy.

Put the whole chain together: the browser loads index.html, main.ts runs, AppModule is bootstrapped, and AppComponent is rendered in place of the <app-root> tag. It's like starting a car: the key is main.ts, the engine is AppModule, and the first wheel to turn is AppComponent.

BrowserModule or CommonModule?

When we create a new module in a minute, you will see CommonModule inside it, not BrowserModule. This pair confuses almost everyone at first.

CommonModule brings the basic directives and pipes you will use in templates all the time: *ngIf, *ngFor, the date pipe and so on. BrowserModule re-exports CommonModule as is and adds the infrastructure the app needs to run in the browser on top.

That infrastructure is set up once. So the rule is simple: BrowserModule only in AppModule, CommonModule in every other module. Break the rule by adding BrowserModule to a module that is loaded later, and Angular throws a "BrowserModule has already been loaded" error that tells you outright to use CommonModule instead.

Creating Our Own Feature Module

As an app grows, piling everything into AppModule is like cramming every employee into a single room. Instead, we group related pieces into a feature module. Let's create one for everything product-related:

ng generate module products

The short form is ng g m products. The CLI creates a single file:

CREATE src/app/products/products.module.ts (194 bytes)

Now let's generate a component inside this module. Notice that we pass the component name together with the folder path:

ng g c products/product-list

This time the last line of the output is UPDATE src/app/products/products.module.ts. The CLI registered the component in the nearest module and didn't touch AppModule at all. Here is the file now:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductListComponent } from './product-list/product-list.component';

@NgModule({
  declarations: [
    ProductListComponent
  ],
  imports: [
    CommonModule
  ]
})
export class ProductsModule { }

ProductListComponent is now on ProductsModule's staff. Since CommonModule is imported, its template can use *ngIf or *ngFor whenever it needs to.

Exposing a Component to the Outside

Now let's write <app-product-list></app-product-list> in app.component.html. The build stops with this error:

error NG8001: 'app-product-list' is not a known element:
1. If 'app-product-list' is an Angular component, then verify that it is part of this module.

Neither side has reached out yet. First, ProductsModule has to expose the component:

@NgModule({
  declarations: [
    ProductListComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    ProductListComponent
  ]
})
export class ProductsModule { }

Then AppModule has to import ProductsModule:

import { ProductsModule } from './products/products.module';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    ProductsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Save, and "product-list works!" shows up inside AppComponent. Notice that ProductListComponent is not in AppModule's declarations. AppComponent doesn't know it directly; it sees it through the door ProductsModule opened.

The Shared Module Idea

Some pieces don't belong to any single feature. A loading indicator, a page title, an empty-list message: products need them, and so do orders. You can't declare them in every feature module, because a component can belong to only one module. The fix is to gather the common pieces in a shared module:

ng g m shared
ng g c shared/loading-spinner --export

The --export flag tells the CLI to add the component to both declarations and exports. Then we add one more line to exports by hand:

@NgModule({
  declarations: [
    LoadingSpinnerComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    LoadingSpinnerComponent,
    CommonModule
  ]
})
export class SharedModule { }

A module can pass along another module it imports, as is. So a feature module that imports SharedModule gets both LoadingSpinnerComponent and the directives of CommonModule with a single line.

Think of the shared module as the office's common tool cabinet. Instead of every department buying its own drill, they take one from the cabinet. Just don't throw everything in there; only pieces that are really used in more than one place belong in it, or the cabinet turns into a storage room before long.

Feature modules have one more benefit: they don't all have to load when the app starts; they can load when the user goes to the related page. That's called lazy loading, and we will cover it in upcoming posts, after routing.

A Small Experiment

Use the CLI to generate a feature module called orders and, inside it, a component called order-summary. Put a few lines of static order summary in the component's template. Then export the component, import OrdersModule into AppModule, and use the <app-order-summary> tag in AppComponent's template.

Then break two things on purpose. First, delete the exports entry in OrdersModule and read the error in the terminal. Once you put it back, add OrderSummaryComponent to AppModule's declarations as well and see the "declared by more than one NgModule" error with your own eyes.

The modules have put the pieces in the right places, but the screen still shows static text. All the ways to carry data from the class into the template, and to bring the user's clicks back to the class, are our next topic: data binding.