One of the best ways to understand Angular is to build a small application instead of learning every concept separately.
In this beginner-friendly tutorial, we'll build a simple Book List application. The application will display books and allow the user to add a new book using a form.
While building it, you'll learn several important Angular concepts including components, services, interfaces, dependency injection, data binding, forms and TypeScript.
What We Are Going to Build
Our application will have two basic features:
- Display a list of books
- Add a new book to the list
Each book will contain:
- ID
- Title
- Author
- Description
The basic application flow will be:
User
↓
Book Component
↓
Book Service
↓
Book Data
↓
Component Template
Prerequisites
Before starting, make sure you have:
- Node.js installed
- npm available
- Angular CLI installed
- A code editor such as Visual Studio Code
- Basic HTML and TypeScript knowledge
You can verify Node.js and npm with:
node --version
npm --version
Step 1: Install Angular CLI
If Angular CLI is not installed, run:
npm install -g @angular/cli
Then verify the installation:
ng version
Step 2: Create the Angular Project
Open a terminal and run:
ng new bookstore
Angular CLI may ask configuration questions such as whether you want routing and which stylesheet format you prefer.
After the project is created:
cd bookstore
ng serve
Open the local address displayed by Angular CLI in your browser. During development this is commonly:
http://localhost:4200
If the Angular starter application appears, your setup is working.
Step 3: Understand the Project Structure
Most of our work will happen inside the src/app directory.
We will create a structure similar to:
src/
└── app/
├── components/
│ └── book/
├── models/
│ └── book.ts
├── services/
│ └── book.service.ts
└── app.component.*
Step 4: Create the Book Model
Instead of using unstructured JavaScript objects everywhere, we'll define the shape of a book using a TypeScript interface.
Run:
ng generate interface models/book
Open:
src/app/models/book.ts
Replace its contents with:
export interface Book {
id: number;
title: string;
author: string;
description: string;
}
Now TypeScript knows exactly which properties a Book should
contain.
Why Use a TypeScript Interface?
Interfaces help catch mistakes while developing the application.
For example:
const book: Book = {
id: 1,
title: 'Learning Angular',
author: 'John Smith',
description: 'Angular for beginners'
};
If a required property is missing or has an incompatible type, TypeScript can report the problem before the application reaches the user.
Step 5: Create the Book Service
Angular services are commonly used to keep data-access and reusable application logic outside UI components.
Generate a service:
ng generate service services/book
Open:
src/app/services/book.service.ts
Add:
import { Injectable } from '@angular/core';
import { Book } from '../models/book';
@Injectable({
providedIn: 'root'
})
export class BookService {
private books: Book[] = [
{
id: 1,
title: 'Learning Angular',
author: 'John Smith',
description: 'A beginner-friendly Angular book'
},
{
id: 2,
title: 'TypeScript Basics',
author: 'Jane Doe',
description: 'Learn the fundamentals of TypeScript'
}
];
getBooks(): Book[] {
return this.books;
}
addBook(book: Book): void {
this.books.push(book);
}
}
What Does the Service Do?
The service has two methods:
getBooks()
returns the current collection of books.
And:
addBook(book)
adds a new book to the collection.
For this beginner example, the data exists only in memory. Refreshing the browser will reset it.
A real application could later replace this in-memory data with an API and database.
Step 6: Create the Book Component
Generate a component:
ng generate component components/book
Angular CLI will create the component files inside the
components/book directory.
Step 7: Add the Component Logic
Open the generated TypeScript component file and use:
import { Component, OnInit } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { Book } from '../../models/book';
import { BookService } from '../../services/book.service';
@Component({
selector: 'app-book',
standalone: true,
imports: [FormsModule],
templateUrl: './book.component.html',
styleUrl: './book.component.css'
})
export class BookComponent implements OnInit {
books: Book[] = [];
newBook = {
title: '',
author: '',
description: ''
};
errorMessage = '';
constructor(private bookService: BookService) {}
ngOnInit(): void {
this.loadBooks();
}
loadBooks(): void {
this.books = this.bookService.getBooks();
}
addBook(): void {
const title = this.newBook.title.trim();
const author = this.newBook.author.trim();
if (!title || !author) {
this.errorMessage = 'Title and author are required.';
return;
}
const book: Book = {
id: this.getNextId(),
title: title,
author: author,
description: this.newBook.description.trim()
};
this.bookService.addBook(book);
this.newBook = {
title: '',
author: '',
description: ''
};
this.errorMessage = '';
}
private getNextId(): number {
if (this.books.length === 0) {
return 1;
}
return Math.max(...this.books.map(book => book.id)) + 1;
}
}
Understanding Dependency Injection
Notice this line:
constructor(private bookService: BookService) {}
We don't manually create the service using:
new BookService()
Instead, Angular's dependency injection system supplies the
BookService instance to the component.
Dependency injection makes applications easier to organize, test and maintain as they grow.
Understanding ngOnInit
Our component implements:
OnInit
and contains:
ngOnInit(): void {
this.loadBooks();
}
Angular calls ngOnInit() after initializing the component, so
we use it to load our initial book data.
Step 8: Create the Book Template
Open:
book.component.html
and add:
<section class="book-container">
<h2>Book List</h2>
@if (books.length === 0) {
<p>No books available.</p>
}
@for (book of books; track book.id) {
<div class="book-card">
<h3>{{ book.title }}</h3>
<p>
<strong>Author:</strong>
{{ book.author }}
</p>
<p>
{{ book.description }}
</p>
</div>
}
<hr>
<h2>Add a New Book</h2>
@if (errorMessage) {
<p class="error">
{{ errorMessage }}
</p>
}
<form (ngSubmit)="addBook()">
<div>
<label for="title">Title</label>
<input
id="title"
name="title"
type="text"
[(ngModel)]="newBook.title"
required>
</div>
<div>
<label for="author">Author</label>
<input
id="author"
name="author"
type="text"
[(ngModel)]="newBook.author"
required>
</div>
<div>
<label for="description">Description</label>
<textarea
id="description"
name="description"
[(ngModel)]="newBook.description">
</textarea>
</div>
<button type="submit">
Add Book
</button>
</form>
</section>
Understanding Angular Interpolation
This syntax:
{{ book.title }}
is called interpolation.
Angular evaluates the expression and displays its value in the HTML.
For example:
<h3>{{ book.title }}</h3>
might render:
<h3>Learning Angular</h3>
Understanding Two-Way Data Binding
Our form uses:
[(ngModel)]="newBook.title"
This connects the form input with the component property.
When the user types into the field, Angular updates the property. If the property changes programmatically, Angular can update the input.
This pattern is commonly known as two-way data binding.
Why Does the Input Need a Name?
Because these controls are inside a form and use ngModel, we've
provided names such as:
name="title"
name="author"
name="description"
This allows Angular's form handling to identify the controls correctly.
Step 9: Add Basic CSS
Open:
book.component.css
and add:
.book-container {
max-width: 700px;
margin: 30px auto;
padding: 20px;
}
.book-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
form div {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: 600;
}
input,
textarea {
box-sizing: border-box;
width: 100%;
padding: 10px;
}
textarea {
min-height: 100px;
}
button {
padding: 10px 18px;
cursor: pointer;
}
.error {
font-weight: 600;
}
Step 10: Display the Book Component
For a standalone Angular application, import the component into your root component.
A minimal example is:
import { Component } from '@angular/core';
import { BookComponent } from './components/book/book.component';
@Component({
selector: 'app-root',
standalone: true,
imports: [BookComponent],
template: '<app-book></app-book>'
})
export class AppComponent {
}
If your project uses the older NgModule structure, the registration process is different. Follow the structure generated by the Angular version used in your project rather than mixing standalone and NgModule examples.
Step 11: Run the Application
Start the development server:
ng serve
Open the local development address shown in the terminal.
You should now see the initial books.
Enter a title, author and description, then click Add Book.
The new book should immediately appear in the list.
How the Application Works
Let's follow what happens when the page loads.
Angular creates BookComponent
↓
ngOnInit() runs
↓
loadBooks() runs
↓
BookService.getBooks()
↓
books[] is populated
↓
Template renders books
When the user adds a book:
User enters book details
↓
ngModel updates newBook
↓
Form calls addBook()
↓
Input is validated
↓
BookService.addBook()
↓
Book added to books[]
↓
Angular updates the UI
Important Limitation: Data Is Not Saved Permanently
Our current service stores books in memory:
private books: Book[] = [...]
Therefore, if you refresh the page, newly added books disappear.
That's expected for this simple tutorial.
A real application would normally store books using something such as:
- A REST API
- ASP.NET Core Web API
- Node.js backend
- SQL database
- Cloud database
How This Could Work With ASP.NET Core
A more realistic architecture could look like:
Angular
↓ HTTP
ASP.NET Core Web API
↓
Entity Framework Core
↓
SQL Server
Angular would be responsible for the user interface, while ASP.NET Core would expose endpoints such as:
GET /api/books
GET /api/books/1
POST /api/books
PUT /api/books/1
DELETE /api/books/1
This would turn our small example into a full CRUD application.
Common Errors
ng: command not found
Angular CLI may not be installed globally or your terminal may not have access to its executable.
Try:
npm install -g @angular/cli
Can't bind to ngModel
If you are using a standalone component, make sure
FormsModule is imported:
import { FormsModule } from '@angular/forms';
and included in the component:
imports: [FormsModule]
Component Is Not Displaying
Check that:
- The component is imported correctly
- The selector is correct
- The selector appears in the parent template
- The application compiled without errors
Books Disappear After Refresh
This is expected because this tutorial stores data only in memory. Persistent storage requires a backend, browser storage or another persistence mechanism.
Ideas to Improve the Project
Once the basic version works, try adding:
- Edit book functionality
- Delete book functionality
- Search by title
- Filter by author
- Form validation messages
- Book categories
- Book cover images
- Angular routing
- REST API integration
- ASP.NET Core backend
- SQL Server database
Frequently Asked Questions
What is an Angular component?
A component represents part of the application's user interface and contains the logic and template required for that part of the UI.
What is an Angular service?
A service is commonly used for reusable logic, shared state or communication with external APIs. Angular's dependency injection system can provide services to components.
What is ngModel?
ngModel can bind form controls to component properties. The
[(ngModel)] syntax provides two-way binding.
What is TypeScript?
TypeScript extends JavaScript with features such as static type checking. Angular applications are commonly developed using TypeScript.
Can Angular connect to ASP.NET Core?
Yes. An Angular application can call an ASP.NET Core Web API using HTTP, making the combination useful for full-stack applications.
Where should I go after this tutorial?
A good next project is to connect the book application to a real REST API and implement complete CRUD operations: create, read, update and delete.
Conclusion
This small book application demonstrates several Angular concepts working together rather than in isolation.
We created a TypeScript model, moved data handling into a service, injected that service into a component, displayed data in a template and created a form using two-way data binding.
Once you understand this flow, you can gradually add routing, validation, HTTP APIs and a backend such as ASP.NET Core to build more realistic applications.