Skip to content
View lessons Login
3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Structure of an Angular application

Structure of an Angular application

The goal of this lesson is to make Angular seem a little less magical, and to gain a basic understanding of how Angular does what it does. What Angular actually does goes very deep, but I think we can gain a basic surface-level understanding of the key ideas. Ideally, we want to get to the stage where we know why certain files exist, and why we are using certain methods, rather than treating things as just some magical thing you need to do to make Angular work.

This lesson will focus primarily on the basic files within an Angular application, and we will dig even further into other key concepts throughout the rest of this module.

Create a new Angular application

So that we have something to reference, we are going to create a new standard Angular application using the Angular CLI. If you do not already have the Angular CLI installed you can do so with the following command:

Terminal window
npm install -g @angular/cli

You can then create a new project with:

Terminal window
ng new

You can use the following configuration:

Terminal window
What name would you like to use for the new workspace and initial project? my-app
Which stylesheet format would you like to use? Sass (SCSS)
Do you want to enable Server-Side Rendering (SSR) and Static Site Generation (SSG/Prerendering)? no

You can then open the project in whatever code editor you like, e.g. Visual Studio Code:

Terminal window
code my-app

I highly encourage you throughout this entire module to reference this application, add code snippets from the lessons, come up with your own code snippets, and just see what happens. Don’t be afraid of breaking the application — we won’t be using this application for anything later, and you can always just delete it and generate a new one anyway.

Start at the beginning

Angular has some special things going on, but in the end… it is basically just a website. The first thing that loads when we access a website is the index.html file. Let’s see what the index.html file for Angular looks like:

src/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>MyApp</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>

A pretty standard looking HTML document. The only curious part is this:

<body>
<app-root></app-root>
</body>

This is where all of the magic for Angular kicks off. Inside of the <body> tag of this document we are adding our root component.

The Root Component

This root component is an Angular component. We always have this component by default. We can find it here:

src/app/app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
title = 'my-app';
}

We are going to talk more about the @Component decorator soon, but notice the selector. That is the tag name that we used in our index.html file. The selector is how Angular knows to compile this component and inject it into the DOM wherever <app-root></app-root> is.

You might also notice that this component links out to a separate file for its template:

src/app/app.component.html
<router-outlet></router-outlet>

The main role of our root component (AppComponent) is to serve as a container for our application. Generally speaking, almost all Angular applications have a single root component, and within that root component will be more components, which might also have more components, making up a tree of nested components:

Angular Application Tree

To create the structure in the diagram above, our template might look like this (assuming that we had already created all these additional components):

<app-layout>
<app-header></app-header>
<app-content>
<app-button></app-button>
</app-content>
</app-layout>

instead of like this:

<router-outlet></router-outlet>

We are going to stick with the <router-outlet> example for the rest of this lesson.

If you serve the application we created with:

Terminal window
ng serve

You will notice that we just have an empty page. That is because we have only added the <router-outlet> which has the responsibility of switching between different components based on the current route in the URL bar. However, we don’t have any components to display yet, so we just have a blank page.

We don’t have to have other components. For example, we could just define a template for the root component directly (go ahead and try this):

<h2>This is my entire app!</h2>

and we would see this rendered out in the browser:

No router outlet

But we generally don’t want to define our entire application inside of one component. This would not allow for routing between different components, and one of the powerful things about Angular is that we can easily break up an application into different components which have their own responsibilities (rather than having one big jumbled mess).

<router-outlet></router-outlet>

Bootstrapping

We understand now that the root component is added to our index.html file and serves as a basic container for the rest of the application. We can add additional components inside of the root component, or we can navigate to those additional components by utilising the <router-outlet> inside of the root component.

But we haven’t really dug deep enough here. We are defining a root component and using it in index.html but there are more steps involved in how this actually happens. An important file in kicking off an Angular application is:

src/main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err),
);

We need to “bootstrap” our root component before we can use it — in the code above we supply our AppComponent as the root component, along with an appConfig from another file. Go find that config file and take a quick look — you will find for now that it is just configured the routes for the application, but more types of configurations can be added as well.

This brings us to the first of those situations I mentioned at the beginning of this course, where there is both a “new” way and an “old” way to do things. We are looking at the “new” way to bootstrap an application using standalone components.

With standalone components we treat the component themselves as the basic building blocks of the application, and our root component is simply a component.

However, you might come across some applications that are not bootstrapped with the AppComponent but rather an NgModule that declares that component.

Before standalone components, a component could only exist within a module which provides its compilation context. An @NgModule is like an isolated little world for the component, it will only know about things that are included in the module it belongs to. A component without an @NgModule would have been like a person without a universe to contain it.

Perhaps even without the historical context you can see how the new approach with standalone components is a simpler mental model.

I’m trying to keep this introduction to Angular high-level and simple for now. We will talk more about NgModule later, but for now, just keep in mind that you might come across Angular applications that are bootstrapped like this instead:

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

Either way, the basic idea of bootstrapping is still essentially the same. To kick off our Angular application, we either bootstrap the root component or the root module which will contain the root component.

This is how that <app-root> tag in our index.html file renders our application.

The approach with standalone is clear enough for now — we just supply the component we want to use as the root component and that is it. But let’s also take a quick look at what it might look like using an @NgModule (even though we won’t be doing this ourselves).

The @NgModule would look something like this:

src/app/app.module.ts
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, AppRoutingModule],
providers: [],
bootstrap: [AppComponent],
})
export class AppModule {}

Notice that this module has our root component as a declaration. That means that our root component is available within this module. If we were to declare two different components in this module, then those components would know about each other’s existence. If we tried to access those components from a component in another module, then it wouldn’t know that these components exist. It would be in its own little separate container world.

We also have some imports. This allows us to import functionality from other modules and make them available in this module — this would allow our AppComponent to access functionality from the AppRoutingModule for example.

We can also actually supply imports directly to our standalone components. You can see that we are doing this already:

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'my-app';
}

With the NgModule concept we declare things within this shared container/world idea, which is how we share functionality between different things. Standalone components still need to do this same thing — in the code above our standalone component wants to import functionality from the RouterOutlet, and it does this by supplying an imports array.

Standalone and NgModules are actually compatible with each other, so you can use a mixture of the two if you like. You can, for example, import an NgModule into a standalone component (or you might just import other standalone components into your standalone component).

Don’t worry if this is feeling like a whole lot of concepts all at once, not all of this information is going to stick right away. Our main goal with these introductory theory lessons is just to get some idea of the basic landscape.

Although we might not know exactly what is happening here at the level of the Angular compiler, we now have a complete picture of how our root component is able to be supplied to the index.html page.

  1. Our root component or root module is bootstrapped in main.ts
  2. Bootstrapping the component creates it and adds it to the DOM, making it available for use in index.html
  3. With our root component bootstrapped, we can then add other components within it to create our application

Creating a new component

At this point, we already have a pretty good picture of how an Angular application “works”. In the following lessons, we will focus on building out our knowledge with additional concepts that we can use to actually create a full application.

However, at this point, we still just have a blank page, so I want to take this walkthrough just a little further. As I mentioned, our root component is really just a container for the rest of our application. What we are going to do now is create a simple new component and add it to our root component so that we actually have something to look at on the screen.

First, we are going to create the component and nest it directly within the root component. Then, instead of just adding it inside of our root component, we are going to route to that component instead.

import { Component } from '@angular/core';
@Component({
selector: 'app-home',
template: ` <p>I am the home component</p> `,
})
export class HomeComponent {}

We have our component defined, now we want to display it in the application. One way we can do this is just to add it to the root component.

<app-home></app-home>
<router-outlet></router-outlet>

We are just trying to display the component alongside the router outlet for now, more on that later. Although we have used the correct selector, you will notice that this does not work. Our code editor will likely complain about it, and the application will fail to compile with this error:

Terminal window
[ERROR] NG8001: 'app-home' is not a known element:
1. If 'app-home' is an Angular component, then verify that it is included in the '@Component.imports' of this component.
2. If 'app-home' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@Component.schemas' of this component to suppress this message. [plugin angular-compiler]

Take a second to think why this might be happening — don’t worry if you’re not sure why.

We are trying to use the HomeComponent inside of our AppComponent… but the AppComponent does not know the HomeComponent exists!

If we are using standalone components, then we need to import the component we want to use directly into the component where we want to use it.

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HomeComponent } from "./home/home.component";
@Component({
selector: 'app-root',
imports: [RouterOutlet, HomeComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'my-app';
}

Now our application will compile, and we will be able to see our new component in the browser!

Home component

Routing to the component

Let’s highlight a problem, and the point of the <router-outlet> by introducing another new component. The intent of our first component was to have it serve as our Home page. Let’s create a page now that will serve as a Settings page.

import { Component } from '@angular/core';
@Component({
selector: 'app-settings',
template: ` <p>I am the settings component</p> `,
})
export class SettingsComponent {}

Let’s do the same thing that we did for our home component — we will add it to the imports for our root component, and add it to the template of the root component. See if you can do this yourself before looking at the solution.

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { HomeComponent } from "./home/home.component";
import { SettingsComponent } from './settings/settings.component';
@Component({
selector: 'app-root',
imports: [RouterOutlet, HomeComponent, SettingsComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.scss'
})
export class AppComponent {
title = 'my-app';
}
<app-home></app-home>
<app-settings></app-settings>
<router-outlet></router-outlet>

We should now see something like this in the browser:

Settings component

Not exactly what we want… we don’t want to display both the home page and the settings page at the same time. This is where the <router-outlet> comes into the picture. Instead of adding these components directly to the root component, we want to route to them.

<router-outlet></router-outlet>

To configure which components should display, and when, we will need to define some routes. We can do this in our src/app/app.routes.ts file. We are going to discuss routing in more detail in a later lesson. The key idea to understand for now is that we can pass some routes into to the Angular router and that will control what the <router-outlet> displays.

import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'home',
loadComponent: () =>
import('./home/home.component').then((m) => m.HomeComponent),
},
{
path: 'settings',
loadComponent: () =>
import('./settings/settings.component').then(
(m) => m.SettingsComponent,
),
},
{
path: '',
redirectTo: 'home',
pathMatch: 'full',
},
];

This array defines which component we want to activate for a particular route. We also have a default path that will redirect to the home route.

We don’t actually need to make any changes here, but for completeness, you might want to take a look at the app.config.ts file:

import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
],
};

This code is set up by default, but this is where the routes we are defining are actually passed to the Angular router.

Now if we go to http://localhost:4200/ (because of our default path) or http://localhost:4200/home we will see the HomeComponent and if we go to http://localhost:4200/settings we will see the SettingsComponent.

As always, there is much more to learn about routing. For now, this is enough for us to get a picture of how <router-outlet> works. It’s important to understand the difference between a component that is displayed within another component by adding it to its template and a routed component.

Going deeper

Now we have two “page” or “routed” components (later we will also refer to these as “smart” components): the home page and the settings page. We change which one of these is displayed based on the current active route.

But we don’t just have routed components. We can have additional components within our page/view components to help keep our application more modular. We could just define the entire template for our home page within the HomeComponent itself, or we could create more components to nest within it.

We are going to create one more component to finish off this lesson. Let’s say we want a WelcomeComponent that just displays the message Hi, Josh! on the home page.

import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
template: ` <p>Hi, Josh!</p> `,
})
export class WelcomeComponent {}
import { Component } from '@angular/core';
import { WelcomeComponent } from './ui/welcome.component';
@Component({
selector: 'app-home',
template: `
<app-welcome />
<p>I am the home component</p>
`,
imports: [WelcomeComponent],
})
export class HomeComponent {}

Now if we look at our home page:

Welcome component

We can see that our WelcomeComponent is being displayed inside of our HomeComponent. This can go as many layers deep as you like — you might also decide to add another component inside of WelcomeComponent.

Our full component tree now looks something like this (again, this is somewhat simplified):

Component tree

The reason we see what we see on screen is because:

  1. Our root component has a <router-outlet> in its template
  2. We have a route set up to display the HomeComponent in the <router-outlet> when the path is /home (or /)
  3. We have added our WelcomeComponent to the template of our HomeComponent

Recap

We’ve covered quite a lot this lesson. The rest of this module is made up of lessons that cover individual concepts in a lot of detail, but the goal of this lesson was to paint the big picture whilst avoiding diving too deep into individual concepts (although that was required to some degree).

I’ve thrown a lot at you, so don’t worry if it hasn’t all stuck right away — we will continue addressing these concepts and more.

    What best describes the typical role of the root component?

    What is required in order to use the app-root tag in our index.html file?

    The only way to display a component is to configure a route for it