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:
npm install -g @angular/cli
You can then create a new project with:
ng new
You can use the following configuration:
✔ 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:
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>
IMPORTANT: The default template for a newly generated Angular application contains a lot of placeholder content. You should replace the entire template with just the code above.
NOTE: To use the <router-outlet>
template in the template, we must have
RouterOutlet
in the imports
array in app.component.ts
(this is already
added for us by default)
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:
NOTE: We will typically use the <router-outlet>
in our applications to
switch between components. In which case, the diagram above would just have the
AppComponent
and the <router-outlet>
as a child node. But, to give a better
sense of the relationship between components, the diagram above does not use
<router-outlet>
and assumes we are not using routing. This way, we can
represent our application as one big tree which is a bit easier to think about
for now.
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:
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:
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).
Make sure to change the template back to the router outlet:
<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.
NOTE: You might see some components marked as standalone: true
or
standalone: false
. Since Angular v19, standalone: true
has been enabled by
default. That means that no configuration is required and all components will be
standalone by default for applications using Angular v19+.
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.
- Our root component or root module is bootstrapped in
main.ts
- Bootstrapping the component creates it and adds it to the DOM, making it available for use in
index.html
- 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.
NOTE: Whenever you see me referencing a folder path that does not exist, you
should create those folders yourself. For example, the following instruction
will require that you create your own home
folder inside of the app
folder.
Create a new file at
src/app/home/home.component.ts
and add the following:
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.
Try adding the
HomeComponent
to the rootAppComponent
template
<app-home></app-home>
<router-outlet></router-outlet>
NOTE: If you have not already deleted all of the default boilerplate from
the app.component.html
file make sure to do that now and only include just the
code above in the template.
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:
✘ [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.
Click here to reveal solution
Solution
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.
Make sure to import the
HomeComponent
into theAppComponent
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';
}
IMPORTANT: There are actually two imports here. One at the top to import
HomeComponent
into the file, but we also need to add it to the imports
array
inside of our @Component
— we need to do this so that the Angular compiler
knows about it.
NOTE: Although you can just type these imports out manually, if you are using the Angular Language Service extension it can do it automatically for you through executing a code action (try right-clicking the error in the template or hitting the keyboard shortcut in your editor for triggering code action)
Now our application will compile, and we will be able to see our new component in the browser!
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.
Create a new file at
src/app/settings/settings.component.ts
and add the following:
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.
Click here to reveal solution
Solution
Import the
SettingsComponent
intoAppCommponent
:
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';
}
Add the
SettingsComponent
to the root components template:
<app-home></app-home>
<app-settings></app-settings>
<router-outlet></router-outlet>
We should now see something like this in the browser:
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.
Remove the two custom components from the root component template:
<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.
Modify
src/app/app.routes.ts
to reflect the following:
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.
Create a component at
src/app/home/ui/welcome.component.ts
and add the following:
import { Component } from '@angular/core';
@Component({
selector: 'app-welcome',
template: ` <p>Hi, Josh!</p> `,
})
export class WelcomeComponent {}
NOTE: The location of this component (i.e. home/ui
) is a convention we
will be using in this course, but it does not have to be here. We will talk
more about this folder structure later and its benefits, but this component
could exist anywhere in your project.
Modify the
HomeComponent
to reflect the following:
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 {}
NOTE: Notice that we are defining the entire component, including the template, in a single file (rather than having a file specifically for the template). You can use either style, but this is the style we will be using in this course.
Now if we look at our home page:
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):
The reason we see what we see on screen is because:
- Our root component has a
<router-outlet>
in its template - We have a route set up to display the
HomeComponent
in the<router-outlet>
when the path is/home
(or/
) - We have added our
WelcomeComponent
to the template of ourHomeComponent
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?
Incorrect.
Incorrect. It is @NgModule's that provide a compilation context
Correct!
What is required in order to use the app-root tag in our index.html file?
Incorrect
Correct!
The only way to display a component is to configure a route for it
Incorrect
Correct! We can also add components directly to the template of another component